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