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.
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.
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.
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;
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);
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.
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);
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);