Java-Code bearbeiten

Mit der JDT-API kann ein Plug-in Klassen oder Schnittstellen erstellen, Methoden zu vorhandenen Typen hinzufügen oder die Methoden für Typen ändern.

Der einfachste Weg zur Änderung von Java-Objekten ist die Verwendung der API für Java-Elemente. Bei der Bearbeitung des unformatierten Quellcodes für ein Java-Element können allgemeinere Methoden verwendet werden.

Code unter Verwendung von Java-Elementen ändern

Kompiliereinheit generieren

Die einfachste Methode, eine Kompiliereinheit programmgestützt zu generieren, ist die Verwendung von IPackageFragment.createCompilationUnit. Sie geben den Namen und den Inhalt der Kompiliereinheit an. Die Kompiliereinheit wird im Paket erstellt, und das neue Objekt ICompilationUnit wird zurückgegeben.

Eine Kompiliereinheit kann generisch erstellt werden, indem im jeweiligen Ordner, der dem Paketverzeichnis entspricht, eine Dateiressource mit der Erweiterung .java erstellt wird. Die Verwendung der API für generische Ressourcen ist eine Alternative für den Zugang zu Java-Tools und wird von den Java-Tools nicht erkannt. Daher wird das Java-Modell erst dann aktualisiert, wenn die Listener-Funktionen für Änderungen generischer Ressourcen benachrichtigt werden und die JDT-Listener-Funktionen das Java-Modell mit der neuen Kompiliereinheit aktualisieren.

Kompiliereinheit ändern

Die meisten einfachen Änderungen an Java-Quellen können über die API für Java-Elemente vorgenommen werden.

Beispielsweise können Sie einen Typ aus einer Kompiliereinheit abfragen. Sobald das Objekt IType vorhanden ist, können Sie dem Typ Quellcode-Member hinzufügen. Hierzu verwenden Sie eines der Protokolle createField, createInitializer, createMethod oder createType. In diesen Methoden werden der Quellcode und Informationen zur Position des Members zur Verfügung gestellt.

Die Schnittstelle ISourceManipulation definiert allgemeine Methoden zur Quellenbearbeitung für Java-Elemente. Hierzu gehören Methoden für das Umbenennen, Versetzen, Kopieren oder Löschen eines Typ-Members.

Arbeitskopien

Code kann durch Ändern der Kompiliereinheit geändert werden (dadurch wird die zu Grunde liegende Datei IFile geändert) oder die Speicherkopie der Kompiliereinheit (d. h. die Arbeitskopie) kann geändert werden.

Eine Arbeitskopie erhalten Sie von einer Kompiliereinheit mit Hilfe der Methode getWorkingCopy. (Bitte beachten Sie, dass im Java-Modell nicht unbedingt eine Kompiliereinheit vorhanden sein muss, damit eine Arbeitskopie erstellt werden kann.)  Der Ersteller einer solchen Arbeitskopie ist dafür verantwortlich, diese Kopie mit Hilfe der Methode discardWorkingCopy zu löschen, wenn sie nicht mehr benötigt wird.

Arbeitskopien modifizieren einen speicherinternen Puffer. Die Methode getWorkingCopy() erstellt einen Standardpuffer. Allerdings können Clients ihre eigene Pufferimplementierung mit Hilfe der Methode getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor) bereitstellen. Clients können den Text dieses Puffers direkt ändern. Wenn sie dies tun, müssen die Clients die Arbeitskopie von Zeit zu Zeit mit dem Puffer synchronisieren, indem Sie die entsprechende Methode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) verwenden.

Schließlich kann eine Arbeitskopie mit Hilfe der Methode commitWorkingCopy auf der Platte gespeichert werden (wodurch die ursprüngliche Kompiliereinheit ersetzt wird).  

Der folgende Codeausschnitt beispielsweise erstellt eine Arbeitskopie für eine Kompiliereinheit, indem ein angepasster Arbeitskopieeigner verwendet wird. Der Ausschnitt ändert den Puffer, gleicht die Änderungen aus, schreibt die Änderungen auf die Platte und löscht schließlich die Arbeitskopie.

    // Ursprüngliche Kompiliereinheit abrufen
    ICompilationUnit originalUnit = ...;
    
    // Arbeitskopieeigner abrufen
    WorkingCopyOwner owner = ...;
    
    // Arbeitskopie erstellen
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Puffer ändern und ausgleichen
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Änderungen festschreiben
    workingCopy.commitWorkingCopy(false, null);
    
    // Arbeitskopie löschen
    workingCopy.discardWorkingCopy();

Arbeitskopien können auch mit Hilfe eines Arbeitskopieeigners von mehreren Clients gemeinsam benutzt werden. Eine Arbeitskopie kann zu einem späteren Zeitpunkt mit Hilfe der Methode findWorkingCopy abgerufen werden. Eine gemeinsam benutzte Arbeitskopie wird somit in der ursprünglichen Kompiliereinheit und in einem Arbeitskopieeigner zugeordnet.

Das folgende Beispiel zeigt, wie Client 1 eine gemeinsam benutzte Arbeitskopie erstellt, Client 2 diese Arbeitskopie abruft, Client 1 die Arbeitskopie löscht und Client 2 beim Abruf der gemeinsam benutzten Arbeitskopie feststellt, dass diese nicht mehr vorhanden ist:

    // Client 1 & 2: Ursprüngliche Kompiliereinheit abrufen
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Arbeitskopieeigner abrufen
    WorkingCopyOwner owner = ...;
    
    // Client 1: Gemeinsam benutzte Arbeitskopie erstellen
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2: Gemeinsam benutzte Arbeitskopie abrufen
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Dieselbe Arbeitskopie
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Gemeinsam benutzte Arbeitskopie löschen
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2: Versuch, nicht mehr vorhandene Arbeitskopie abzurufen
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Code unter Verwendung von DOM/AST API ändern

Zum Erstellen einer Kompiliereinheit (CompilationUnit) gibt es drei Möglichkeiten. Die erste Möglichkeit besteht in der Verwendung von ASTParser. Die zweite Möglichkeit besteht in der Verwendung von ICompilationUnit#reconcile(...). Bei der dritten Möglichkeit müssen Sie von Grund auf anfangen, indem Sie die Factorymethoden unter AST (Abstract Syntax Tree) verwenden.

Abstrakte Syntaxstruktur (Abstract Syntax Tree, AST) aus vorhandenem Quellcode erstellen

Ein Exemplar von ASTParser muss mit ASTParser.newParser(int) erstellt werden.

Der Quellcode wird mit Hilfe einer der folgenden Methoden für ASTParser zur Verfügung gestellt: Anschließend wird die AST erstellt, indem createAST(IProgressMonitor) aufgerufen wird.

Das Ergebnis ist eine AST mit den jeweils korrekten Quellenpositionen für die einzelnen Knoten. Die Auflösung von Bindungen (Bindings) muss vor dem Erstellen der Struktur mit Hilfe von setResolveBindings(boolean) angefordert werden. Das Auflösen der Bindings ist eine aufwendige Operation und sollte daher nur durchgeführt werden, wenn dies erforderlich ist. Sobald die Baumstruktur modifiziert wurde, gehen alle Positionen und Bindings verloren.

Abstrakte Syntaxstruktur (AST) durch Ausgleichen einer Arbeitskopie erstellen

Wenn eine Arbeitskopie nicht konsistent ist (also geändert wurde), kann eine AST durch Aufrufen der Methode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) erstellt werden. Um das Erstellen der AST anzufordern, rufen Sie die Methode reconcile(...) mit AST.JLS2 als erstem Parameter auf.

Die entsprechenden Bindungen werden nur dann berechnet, wenn die Anforderungskomponente (Requester) für Fehler aktiv ist oder die Fehlererkennung erzwungen wird. Das Auflösen der Bindings ist eine aufwendige Operation und sollte daher nur durchgeführt werden, wenn dies erforderlich ist. Sobald die Baumstruktur modifiziert wurde, gehen alle Positionen und Bindings verloren.

Von Grund auf

Es ist möglich, eine CompilationUnit von Grund auf mit Hilfe der Factory-Methoden unter AST zu erstellen. Diese Methoden beginnen mit Neu.... Das folgende Beispiel erstellt eine HelloWorld-Klasse.

Der erste Ausschnitt ist die generierte Ausgabe:

	package example;
import java.util.*;
public class HelloWorld {
public static void main(String[] args) {
			System.out.println("Hello" + " world");
		}
	}

Der folgende Ausschnitt ist der entsprechende Code, der die Ausgabe generiert.

		AST ast = new AST();
		CompilationUnit unit = ast.newCompilationUnit();
		PackageDeclaration packageDeclaration = ast.newPackageDeclaration();
		packageDeclaration.setName(ast.newSimpleName("example"));
		unit.setPackage(packageDeclaration);
		ImportDeclaration importDeclaration = ast.newImportDeclaration();
		QualifiedName name = 
			ast.newQualifiedName(
				ast.newSimpleName("java"),
				ast.newSimpleName("util"));
		importDeclaration.setName(name);
		importDeclaration.setOnDemand(true);
		unit.imports().add(importDeclaration);
		TypeDeclaration type = ast.newTypeDeclaration();
		type.setInterface(false);
		type.setModifiers(Modifier.PUBLIC);
		type.setName(ast.newSimpleName("HelloWorld"));
		MethodDeclaration methodDeclaration = ast.newMethodDeclaration();
		methodDeclaration.setConstructor(false);
		methodDeclaration.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
		methodDeclaration.setName(ast.newSimpleName("main"));
		methodDeclaration.setReturnType(ast.newPrimitiveType(PrimitiveType.VOID));
		SingleVariableDeclaration variableDeclaration = ast.newSingleVariableDeclaration();
		variableDeclaration.setModifiers(Modifier.NONE);
		variableDeclaration.setType(ast.newArrayType(ast.newSimpleType(ast.newSimpleName("String"))));
		variableDeclaration.setName(ast.newSimpleName("args"));
		methodDeclaration.parameters().add(variableDeclaration);
		org.eclipse.jdt.core.dom.Block block = ast.newBlock();
		MethodInvocation methodInvocation = ast.newMethodInvocation();
		name = 
			ast.newQualifiedName(
				ast.newSimpleName("System"),
				ast.newSimpleName("out"));
		methodInvocation.setExpression(name);
		methodInvocation.setName(ast.newSimpleName("println")); 
		InfixExpression infixExpression = ast.newInfixExpression();
		infixExpression.setOperator(InfixExpression.Operator.PLUS);
		StringLiteral literal = ast.newStringLiteral();
		literal.setLiteralValue("Hello");
		infixExpression.setLeftOperand(literal);
		literal = ast.newStringLiteral();
		literal.setLiteralValue(" world");
		infixExpression.setRightOperand(literal);
		methodInvocation.arguments().add(infixExpression);
		ExpressionStatement expressionStatement = ast.newExpressionStatement(methodInvocation);
		block.statements().add(expressionStatement);
		methodDeclaration.setBody(block);
		type.bodyDeclarations().add(methodDeclaration);
		unit.types().add(type);

Zusätzliche Positionen abrufen

Der Knoten DOM/AST enthält lediglich ein Positionspaar (die Anfangsposition und die Länge des Knotens). Dies ist nicht in allen Fällen ausreichend. Temporäre Positionen rufen Sie mit der IScanner-API ab. Angenommen, Sie haben ein InstanceofExpression, für das Sie die Positionen des instanceof-Operators kennen wollen. Sie können dazu nun die folgende Methode schreiben:
	private int[] getOperatorPosition(Expression expression, char[] source) {
		if (expression instanceof InstanceofExpression) {
			IScanner scanner = ToolFactory.createScanner(false, false, false, false);
			scanner.setSource(source);
			int start = expression.getStartPosition();
			int end = start + expression.getLength();
			scanner.resetTo(start, end);
			int token;
			try {
				while ((token = scanner.getNextToken()) != ITerminalSymbols.TokenNameEOF) {
					switch(token) {
						case ITerminalSymbols.TokenNameinstanceof:
							return new int[] {scanner.getCurrentTokenStartPosition(), scanner.getCurrentTokenEndPosition()};
					}
				}
			} catch (InvalidInputException e) {
			}
		}
return null;
	}
Der IScanner wird verwendet, um die Eingabequelle in Token zu teilen. Jedes Token besitzt einen spezifischen Wert, der in der ITerminalSymbols-Schnittstelle definiert ist. Es ist einfach zu iterieren und das korrekte Token abzurufen. Es wird außerdem empfohlen, dass Sie den Scanner verwenden, wenn Sie die Position des Superkennworts in einem SuperMethodInvocation suchen wollen.

Quellcodeänderungen

Einige Quellcodeänderungen sind über die API für Java-Elemente nicht möglich. Eine allgemeinere Methode zur Bearbeitung von Quellcode (z. B. das Ändern des Quellcodes für vorhandene Elemente) wird durch die Verwendung des unformatierten Quellcodes der Kompiliereinheit und der API für erneutes Schreiben von DOM/AST bereitgestellt.

Für das erneute Schreiben von DOM/AST stehen zwei API-Gruppen zur Verfügung: Eine Gruppe für erneutes Schreiben mit Beschreibungen und eine Gruppe für erneutes Schreiben mit Änderungen.

Die beschreibende API führt keine Änderungen an der AST aus, sondern verwendet die API ASTRewrite, um die Beschreibungen von Änderungen zu generieren. Die Funktion für erneutes Schreiben der AST erfasst Beschreibungen von Änderungen an Knoten und setzt diese Beschreibungen in Textbearbeitungen um, die anschließend auf die ursprüngliche Quelle angewendet werden können.

   // Erstellen eines Dokuments
   ICompilationUnit cu = ... ; // Inhalt ist "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // Erstellen von DOM/AST aus ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // Erstellen von ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // Beschreibung der Änderung
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // Berechnung der Textbearbeitungen
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // Berechnung des neuen Quellcodes
   edits.apply(document);
   String newSource = document.get();

   // Aktualisierung der Kompiliereinheit
   cu.getBuffer().setContents(newSource);

Mit der Änderungs-API kann die AST wie folgt direkt geändert werden:

   // Erstellen eines Dokuments
   ICompilationUnit cu = ... ; // Inhalt ist "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // Erstellen von DOM/AST aus ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // Starten der Aufzeichnung der Änderungen
   astRoot.recordModifications();

   // Ändern der AST
   TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0)
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   typeDeclaration.setName(newName);

   // Berechnung der Textbearbeitungen
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // Berechnung des neuen Quellcodes
   edits.apply(document);
   String newSource = document.get();

   // Aktualisierung der Kompiliereinheit
   cu.getBuffer().setContents(newSource);

Auf Änderungen in Java-Elementen reagieren

Wenn Ihre Plug-ins Änderungen, die nach dem Fakt an Java-Elementen vorgenommen wurden, kennen müssen, können Sie ein Java-Objekt IElementChangedListener mit JavaCore registrieren.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Sie können genauere Angaben machen und die Ereignistypen angeben, die Sie interessieren, indem Sie addElementChangedListener(IElementChangedListener, int) verwenden.

Wenn Sie beispielsweise die Listenerfunktion lediglich für Ereignisse während einer Ausgleichsoperation verwenden wollen:

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter(), ElementChangedEvent.POST_RECONCILE);

Zwei Ereignistypen werden von JavaCore unterstützt:

Das Konzept der Listener-Funktionen für Änderungen an Java-Elementen ähnelt dem der Listener-Funktionen für Ressourcenänderungen, die unter Ressourcenänderungen protokollieren beschrieben sind. Der folgende Ausschnitt implementiert ein Berichtsprogramm für Änderungen an Java-Elementen, das die Element-Deltas an der Systemkonsole ausgibt.

   public class MyJavaElementChangeReporter implements IElementChangedListener {
      public void elementChanged(ElementChangedEvent event) {
         IJavaElementDelta delta= event.getDelta();
         if (delta != null) {
            System.out.println("delta received: ");
            System.out.print(delta);
         }
      }
   }

IJavaElementDelta umfasst das Element, das geändert wurde sowie Markierungen, die die vorgenommene Änderung beschreiben. Die Deltabaumstruktur gründet in den meisten Fällen auf der Java-Modellstufe. Clients müssen anschließend mit Hilfe von getAffectedChildren zu diesem Delta navigieren, um die geänderten Projekte zu ermitteln.

Die folgende Beispielmethode durchquert ein Delta und druckt die Elemente, die hinzugefügt, entfernt und geändert wurden:

    void traverseAndPrint(IJavaElementDelta delta) {
        switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " was added");
                break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " was removed");
                break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " was changed");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("The change was in its children");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("The change was in its content");
                }
                /* Others flags can also be checked */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Mehrere Operationsarten können einen Java-Hinweis für die Elementänderung auslösen. Einige Beispiele:

Ähnlich wieIResourceDelta können Deltas für Java-Elemente unter Verwendung von IWorkspaceRunnable im Stapelbetrieb verarbeitet werden. Die Deltas, die aus mehreren Java-Modelloperationen resultieren, die in einer IWorkspaceRunnable ausgeführt werden, werden gemischt und umgehend berichtet.  

JavaCore stellt eine Methode run zur Verfügung, mit der Änderungen an Java-Elementen im Stapelbetrieb verarbeitet werden können.

Das folgende Codefragment löst beispielsweise 2 Ereignisse für Änderungen an Java-Elementen aus:

    // Paket abrufen
    IPackageFragment pkg = ...;
    
    // 2 Kompiliereinheit erstellen
    	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
    	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Das folgende Codefragment löst hingegen ein Java-Ereignis für die Elementänderung aus:

    // Paket abrufen
    IPackageFragment pkg = ...;
    
    // 2 Kompiliereinheit erstellen
    JavaCore.run(
        new IWorkspaceRunnable() {
 	        public void run(IProgressMonitor monitor) throws CoreException {
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);
 	        }
        },
        null);