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 für das Löschen dieser mit Hilfe der Methode destroy, 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(IProgressMonitor, IBufferFactory, IProblemRequestor) bereitstellen. Clients können den Text dieses Puffers direkt ändern. Ist dies der Falls, müssen Clients die Arbeitskopie von Zeit zu Zeit mit dem Puffer mit Hilfe der Methode reconcile() oder reconcile(boolean,IProgressMonitor) synchronisieren.

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

Beispielsweise erstellt das folgende Code-Snippet eine Arbeitskopie an einer Kompiliereinheit, indem eine angepasste Puffer-Factory verwendet wird. Das Snippet modifiziert 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 = ...;
    
    // Puffer-Factory abrufen
    IBufferFactory factory = ...;
    
    // Arbeitskopie erstellen
    IWorkingCopy workingCopy = (IWorkingCopy)originalUnit.getWorkingCopy(null, factory, null);
    
    // Puffer ändern und ausgleichen
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile();
    
    // Änderungen festschreiben
    workingCopy.commit(false, null);
    
    // Arbeitskopie löschen
    workingCopy.destroy();

Arbeitskopien können ebenfalls von mehreren Clients gemeinsam benutzt werden. Eine gemeinsam benutzte Arbeitskopie wird mit Hilfe der Methode getSharedWorkingCopy erstellt und kann später mit Hilfe der Methode findSharedWorkingCopy abgerufen werden. Eine gemeinsam benutzte Arbeitskopie wird somit in der ursprünglichen Kompiliereinheit und in einer Puffer-Factory zugeordnet.

Im Folgenden wird angezeigt, wie Client 1 eine gemeinsam benutzte Arbeitskopie erstellt, Client 2 diese Arbeitskopie abruft, Client 1 die Arbeitskopie löscht und Client 2, der die gemeinsam benutzte Arbeitskopie abzurufen versucht bemerkt, dass diese nicht mehr vorhanden ist:

    // Client 1 & 2: Ursprüngliche Kompiliereinheit abrufen
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Puffer-Factory abrufen
    IBufferFactory factory = ...;
    
    // Client 1: Gemeinsam benutzte Arbeitskopie erstellen
    IWorkingCopy workingCopyForClient1 = (IWorkingCopy)originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Client 2: Gemeinsam benutzte Arbeitskopie abrufen
    IWorkingCopy workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
     
    // Dies ist dieselbe Arbeitskopie
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Gemeinsam benutzte Arbeitskopie löschen
    workingCopyForClient1.destroy();
    
    // Client 2: Abrufen der gemeinsam benutzten Arbeitskopie versuchen und feststellen, dass sie Null ist
    workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
    assert workingCopyForClient2 == null;

Code unter Verwendung von DOM/AST API ändern

Sie können eine CompilationUnit auf drei Wegen erstellen. Für den ersten verwenden Sie eine vorhandene Kompiliereinheit. Der zweite Weg besteht in der Verwendung einer vorhandenen Klassendatei. Bei der dritten Möglichkeit müssen Sie von Grund auf anfangen, indem Sie die Factorymethoden unter AST (Abstract Syntax Tree) verwenden.

Eine Baumstruktur der abstrakten Syntax (Abstract Syntax Tree - AST) aus einer vorhandenen Kompiliereinheit erstellen

Sie erreichen dies mit den Methoden zum syntaktischen Analysieren unter AST: Diese Methoden legen die Positionen für sämtliche Knoten in der resultierenden Baumstruktur korrekt fest. Die Auflösung von Bindings muss vor der Erstellung der Baumstruktur 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.

Baumstruktur der abstrakten Syntax (Abstract Syntax Tree - AST) aus einer vorhandenen Klassendatei erstellen

Sie erreichen dies mit der Methode zum syntaktischen Analysieren unter AST: Diese Methode legt die Positionen für sämtliche Knoten in der resultierenden Baumstruktur korrekt fest. Die Auflösung von Bindings muss vor der Erstellung der Baumstruktur 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.

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.

Das erste Snippet ist die generierte Ausgabe:

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

Das folgende Snippet 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 generisch ändern

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 des Java-Dokumentobjektmodells (Document Object Model - DOM) bereitgestellt.

Hierzu gehören die folgenden Techniken:

   // Quelle für eine Kompiliereinheit abrufen
   String contents = myCompilationUnit.getBuffer().getContents();

   // JDOM erstellen, das bearbeitet werden kann.
   myJDOM = new DOMFactory();
   myDOMCompilationUnit = myJDOM.createCompilationUnit(contents, "MyClass");

   // Mit dem JDOM-Knotenprotokoll in der Struktur der 
   // Kompiliereinheit navigieren und diese bearbeiten. 
   ...
   // Sobald Änderungen an allen Knoten vorgenommen wurden, die 
   // Quelle aus dem DOM-Knoten der Kompiliereinheit zurückerhalten.
   String newContents = myDOMCompilationUnit.getContents();

   // Diesen Code in das Element der Kompiliereinheit zurückstellen
   myCompilationUnit.getBuffer().setContents(newContents);

   // Den Puffer in der Datei speichern.
   myCompilationUnit.save(null, false);

Bei dieser Methode werden unter Umständen Problemmarkierungen zu falschen Zeilennummern zugeordnet, da die Java-Elemente nicht direkt aktualisiert wurden.

Methoden und Felder sind die detailliertesten Ebenen des Java-Elementmodells. Die durch den Compiler verwendete Baumstruktur der abstrakten Syntax (Abstract Syntax Tree - AST) ist nicht als API verfügbar. Daher sind die durch JDT verwendeten Methoden, mit denen ein Parsing für Quellen in programmatischen Strukturen ausgeführt wird, gegenwärtig nicht als API verfügbar.

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 Listener-Funktion lediglich für Ereignisse vor der Ausführung der Erstellungsprogramme verwenden wollen:

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

Drei 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. Das folgende Snippet 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 Kompiliereinheiten 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 Kompiliereinheiten 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);

Copyright IBM Corporation und Andere 2000, 2003. Alle Rechte vorbehalten.