Moduł dodatkowy może używać interfejsu API JDT do tworzenia klas lub interfejsów, dodawania metod do istniejących typów lub modyfikowania metod w typach.
Najprostszym sposobem zmodyfikowania obiektów Java jest użycie interfejsu API elementów Java. Bardziej ogólne techniki mogą być używane do pracy z surowym kodem źródłowym elementu Java.
Najprostszym sposobem programowego generowania jednostki kompilacji jest użycie metody IPackageFragment.createCompilationUnit. Użytkownik określa nazwę i treść jednostki kompilacji. Jednostka kompilacji zostaje utworzona wewnątrz pakietu i zwracany jest nowy obiekt klasy implementującej interfejs ICompilationUnit.
Jednostkę kompilacji można wygenerować ogólnie przez utworzenie zasobu będącego plikiem o rozszerzeniu ".java" w odpowiednim folderze, który odpowiada katalogowi pakietu. Użycie ogólnego interfejsu API zasobów jest furtką do narzędzi Java, a zatem model Java jest aktualizowany dopiero wtedy, gdy zostaną powiadomione ogólne obiekty nasłuchiwania zmian zasobów, a obiekty nasłuchiwania JDT zaktualizują model Java o nową jednostkę kompilacji.
Większość prostych modyfikacji kodu źródłowego Java można wprowadzać przy użyciu interfejsu API elementów Java.
Na przykład można utworzyć zapytanie o typ z jednostki kompilacji. Jeśli istnieje typ IType, można użyć protokołów takich jak createField, createInitializer, createMethod lub createType w celu dodania składowych kodu źródłowego do typu. Kod źródłowy i informacje o położeniu składowej są udostępniane w tych metodach.
Interfejs ISourceManipulation definiuje najczęściej występujące operacje manipulowania kodem źródłowym elementów Java. Dotyczy to metod służących do zmiany nazwy, przenoszenia, kopiowania lub usuwania składowej typu.
Kod można modyfikować, manipulując jednostką kompilacji (co powoduje zmianę bazowego obiektu IFile) lub modyfikując przechowywaną w pamięci kopię jednostki kompilacji, nazywaną kopią roboczą.
Kopię roboczą jednostki kompilacji można uzyskać za pomocą metody getWorkingCopy. (Jednostka kompilacji nie musi istnieć w modelu Java, aby kopia robocza mogła zostać utworzona). Każdy użytkownik tworzący taką kopię roboczą jest odpowiedzialny za jej usunięcie, kiedy już przestanie być potrzebna (przy użyciu metody discardWorkingCopy).
Kopie robocze modyfikują bufor pamięci. Metoda getWorkingCopy() tworzy domyślny bufor, ale klienci mogą udostępniać własne implementacje buforu przy użyciu metody getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Klienci mogą bezpośrednio manipulować tekstem tego buforu. Jeśli to robią, muszą od czasu do czasu synchronizować kopię roboczą z buforem przy użyciu którejś z następujących metod: reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).
Kopię robocza można zapisać na dysku (zastępując pierwotną jednostkę kompilacji) przy użyciu metody commitWorkingCopy.
Na przykład poniższy fragment kodu tworzy kopię roboczą dla jednostki kompilacji przy użyciu dostosowanego właściciela kopii roboczej. Fragment kodu modyfikuje bufor, uzgadnia zmiany, zatwierdza zmiany i zapisuje je na dysku oraz ostatecznie usuwa kopię roboczą.
// Pobieranie pierwotnej jednostki kompilacji ICompilationUnit originalUnit = ...; // Pobieranie właściciela kopii roboczej WorkingCopyOwner owner = ...; // Tworzenie kopii roboczej ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null); // Modyfikowanie buforu i uzgadnianie IBuffer buffer = ((IOpenable)workingCopy).getBuffer(); buffer.append("class X {}"); workingCopy.reconcile(NO_AST, false, null, null); // Zatwierdzanie zmian workingCopy.commitWorkingCopy(false, null); // Niszczenie kopii roboczej workingCopy.discardWorkingCopy();
Kopie robocze mogą być także współużytkowane przez kilku klientów przy użyciu właściciela kopii roboczej. Kopię roboczą można później wydobyć przy użyciu metody findWorkingCopy. Współużytkowana kopia robocza jest zatem powiązana z pierwotną jednostką kompilacji i właścicielem kopii roboczej.
Poniższy fragment pokazuje, jak klient 1 tworzy współużytkowaną kopię roboczą, klient 2 wydobywa tę kopię roboczą, klient 1 usuwa kopię roboczą, a klient 2 podczas próby wydobycia tej współużytkowanej kopii roboczej stwierdza, że ona już nie istnieje:
// Klient 1 i 2: Pobiera pierwotną jednostkę kompilacji ICompilationUnit originalUnit = ...; // Klient 1 i 2: Pobiera właściciela kopii roboczej WorkingCopyOwner owner = ...; // Klient 1: Tworzy współużytkowaną kopię roboczą ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null); // Klient 2: Wydobywa współużytkowaną kopię roboczą ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner); // To jest ta sama kopia robocza assert workingCopyForClient1 == workingCopyForClient2; // Klient 1: Usuwa kopię roboczą workingCopyForClient1.discardWorkingCopy(); // Klient 2: Próbuje wydobyć współużytkowaną kopię roboczą i stwierdza, że ma ona wartość NULL workingCopyForClient2 = originalUnit.findWorkingCopy(owner); assert workingCopyForClient2 == null;
Jednostkę kompilacji CompilationUnit można utworzyć od podstaw, używając metod fabryki drzewa AST. Nazwy tych metod rozpoczynają się od new.... Poniżej podano przykład kodu, który tworzy klasę HelloWorld.
Pierwszy fragment kodu to wygenerowane dane wyjściowe:
package example;
import java.util.*;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello" + " world");
}
}
Następujący fragment kodu jest kodem generującym te dane wyjściowe.
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; }Interfejs IScanner jest używany do rozdzielania wejściowego kodu źródłowego na elementy. Każdy element ma określoną wartość, która jest zdefiniowana w interfejsie ITerminalSymbols. Iterowanie i wydobycie właściwego elementu jest całkiem proste. Zalecane jest również użycie skanera, jeśli konieczne jest znalezienie pozycji słowa kluczowego super w węźle reprezentowanym przez klasę SuperMethodInvocation.
Niektóre modyfikacje kodu źródłowego nie są udostępniane poprzez interfejs API elementów Java. Kod źródłowy można edytować w bardziej ogólny sposób (na przykład zmieniać kod źródłowy dla istniejących elementów), używając surowego kodu źródłowego jednostki kompilacji i interfejsu API rewrite struktury DOM/AST.
Na potrzeby wykonywania ponownego zapisywania struktury DOM/AST istnieją dwa zestawy klas interfejsu API ponownego zapisywania: opisowy i modyfikujący.
Opisowy interfejs API nie modyfikuje drzewa AST, lecz używa klasy ASTRewrite do generowania opisów modyfikacji. Klasa ASTRewrite zbiera opisy modyfikacji węzłów i przekształca te opisy w operacje edycji tekstu, które następnie mogą być zastosowane względem oryginalnego kodu źródłowego.
// tworzenie dokumentu
ICompilationUnit cu = ... ; // treść to "public class X {\n}"
String source = cu.getBuffer().getContents();
Document document= new Document(source);
// tworzenie struktury DOM/AST na podstawie obiektu typu ICompilationUnit
ASTParser parser = ASTParser.newParser(AST.JLS2);
parser.setSource(cu);
CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);
// tworzenie instancji klasy ASTRewrite
ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());
// opis zmiany
SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
SimpleName newName = astRoot.getAST().newSimpleName("Y");
rewrite.replace(oldName, newName, null);
// obliczanie modyfikacji tekstu
TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));
// obliczanie nowego kodu źródłowego
edits.apply(document);
String newSource = document.get();
// aktualizacja jednostki kompilacji
cu.getBuffer().setContents(newSource);
Modyfikujący interfejs API pozwala bezpośrednio modyfikować drzewo AST w następujący sposób:
// tworzenie dokumentu ICompilationUnit cu = ... ; // treść to "public class X {\n}" String source = cu.getBuffer().getContents(); Document document= new Document(source); // tworzenie struktury DOM/AST na podstawie obiektu typu ICompilationUnit ASTParser parser = ASTParser.newParser(AST.JLS2); parser.setSource(cu); CompilationUnit astRoot = (CompilationUnit) parser.createAST(null); // uruchamianie rejestracji modyfikacji astRoot.recordModifications(); // modyfikowanie drzewa AST TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0) SimpleName newName = astRoot.getAST().newSimpleName("Y"); typeDeclaration.setName(newName); // obliczanie modyfikacji tekstu TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true)); // obliczanie nowego kodu źródłowego edits.apply(document); String newSource = document.get(); // aktualizacja jednostki kompilacji cu.getBuffer().setContents(newSource);
Jeśli moduł dodatkowy musi wiedzieć o zachodzących zmianach elementów Java, można zarejestrować obiekt nasłuchiwania implementujący interfejs IElementChangedListener w klasie JavaCore.
JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());
Można także określić typ zdarzeń, których ma dotyczyć nasłuchiwanie, używając metody addElementChangedListener(IElementChangedListener, int).
Jeśli na przykład ma być prowadzone tylko nasłuchiwanie zdarzeń zachodzących podczas operacji uzgadniania:
JavaCore.addElementChangedListener(new MyJavaElementChangeReporter(), ElementChangedEvent.POST_RECONCILE);
Istnieją dwa rodzaje zdarzeń obsługiwane przez klasę JavaCore:
Obiekty nasłuchiwania zmian elementów Java są koncepcyjnie podobne do obiektów nasłuchiwania zmian zasobów (opisanych w sekcji Śledzenie zmian zasobów). Poniższy fragment kodu implementuje obiekt zgłaszania zmian elementów Java, wyświetlający zmiany (delta) elementów w konsoli systemowej.
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); } } }
Zmienna typu IJavaElementDelta zawiera element, który został zmieniony, oraz flagi opisujące rodzaj dokonanej zmiany. Przez większość czasu drzewo delta jest zakorzenione na poziomie modelu Java. Klienci muszą następnie przejść do wartości delta, używając metody getAffectedChildren, aby dowiedzieć się o zmianach w projektach.
Metoda użyta w poniższym przykładzie przechodzi do wartości delta i wyświetla elementy, które zostały dodane, usunięte i zmienione.
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"); } /* Inne flagi mogą być także sprawdzane */ break; } IJavaElementDelta[] children = delta.getAffectedChildren(); for (int i = 0; i < children.length; i++) { traverseAndPrint(children[i]); } }
Istnieje wiele rodzajów operacji, które mogą wyzwalać powiadomienie o zmianie elementu Java. Poniżej podano kilka przykładów.
Podobnie jak w przypadku obiektów typu IResourceDelta wartości delta elementów Java mogą być zgłaszane grupowo przy użyciu interfejsu IWorkspaceRunnable. Wartości delta będące rezultatem kilku operacji modelu Java uruchamianych wewnątrz obiektu typu IWorkspaceRunnable są scalane i zgłaszane jednocześnie.
Klasa JavaCore udostępnia metodę run na potrzeby grupowego zgłaszania zmian elementów Java.
Na przykład następujący fragment kodu wyzwoli dwa zdarzenia zmiany elementów Java:
// Pobieranie pakietu IPackageFragment pkg = ...; // Tworzenie dwóch jednostek kompilacji ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null); ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);
Natomiast następujący fragment kodu wyzwoli jedno zdarzenie zmiany elementu Java:
// Pobieranie pakietu IPackageFragment pkg = ...; // Tworzenie dwóch jednostek kompilacji 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);