Il plugin dell'utente può utilizzare l'API JDT per creare classi o interfacce, aggiungere metodi a tipi esistenti o modificare il metodo per i tipi.
Il modo più semplice per modificare oggetti Java consiste nell'utilizzo dell'API di elemento Java. Altre tecniche generiche possono essere utilizzate per gestire il codice di origine primitivo di un elemento Java.
Il modo più semplice per generare in modo programmatico un'unità di compilazione consiste nell'utilizzo di IPackageFragment.createCompilationUnit. L'utente specifica il nome e il contenuto dell'unità di compilazione. L'unità viene creata all'interno del pacchetto e viene restituita la nuova ICompilationUnit.
Un'unità di compilazione può essere creata genericamente mediante la creazione di una risorsa file con estensione ".java" nella cartella appropriata corrispondente alla directory del pacchetto. L'utilizzo di API di risorse generiche si colloca alle spalle della strumentazione Java, in modo che il modello Java non verrà aggiornato fino a quando i listener di modifica delle risorse generiche non ricevono la notifica e i listener JDT non aggiornano il modello Java con la nuova unità di compilazione.
Le modifiche più semplici al codice di origine Java possono essere apportate utilizzando l'API di elemento Java.
Ad esempio, è possibile eseguire la query di un tipo da un'unità di compilazione. Una volta ottenuto l'IType, è possibile utilizzare protocolli come createField, createInitializer, createMethod, o createType per aggiungere al tipo membri di codice di origine. Il codice di origine e le informazioni relative al percorso del membro vengono forniti in questi metodi.
L'interfaccia ISourceManipulation definisce modifiche comuni del codice di origine per elementi Java. Sono compresi metodi per la ridenominazione, lo spostamento, la copia o l'eliminazione del membro di un tipo.
Il codice può essere modificato cambiando l'unità di compilazione (e quindi l'IFile sottostante) o una copia dell'unità di compilazione presente in memoria, definita copia di lavoro.
Una copia di lavoro viene ricavata dall'unità di compilazione mediante il metodo getWorkingCopy. Per la creazione di una copia di lavoro, l'unità di compilazione non deve trovarsi necessariamente nel modello Java. Chiunque crei una copia di lavoro è responsabile della sua eliminazione quando la copia non è più necessaria, mediante il metodo discardWorkingCopy.
Le copie di lavoro modificano il buffer. Il metodo getWorkingCopy() crea un buffer predefinito, ma i client possono fornire una propria implementazione del buffer utilizzando il metodo getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). I client possono modificare il testo di questi buffer direttamente. In questo caso, devono sincronizzare la copia di lavoro con il buffer di volta in volta utilizzando il metodo reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).
Infine, una copia di lavoro può essere salvata su disco (sostituendo l'unità di compilazione originale) utilizzando il metodo commitWorkingCopy.
Ad esempio, il frammento di codice riportato di seguito crea una copia di lavoro sull'unità di compilazione mediante un proprietario della copia di lavoro personalizzata. Il frammento modifica il buffer, riconcilia le modifiche, sincronizza le modifiche sul disco e infine elimina la copia di lavoro.
// Get original compilation unit ICompilationUnit originalUnit = ...; // Get working copy owner WorkingCopyOwner owner = ...; // Create working copy ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null); // Modify buffer and reconcile IBuffer buffer = ((IOpenable)workingCopy).getBuffer(); buffer.append("class X {}"); workingCopy.reconcile(NO_AST, false, null, null); // Commit changes workingCopy.commitWorkingCopy(false, null); // Destroy working copy workingCopy.discardWorkingCopy();
Le copie di lavoro possono anche essere condivise da vari client utilizzando un proprietario della copia di lavoro. Una copia di lavoro può essere richiamata in un secondo momento utilizzando il metodo findWorkingCopy. Una copia di lavoro condivisa viene quindi codificata sull'unità di compilazione originale e su un proprietario della copia di lavoro.
L'esempio di seguito riportato mostra come il client 1 crei una copia di lavoro condivisa, il client 2 richiami questa copia di lavoro, il client 1 elimini la copia di lavoro e il client 2 tenti di richiamare gli avvisi della copia di lavoro condivisa che non esistono più:
// Client 1 & 2: Get original compilation unit ICompilationUnit originalUnit = ...; // Client 1 & 2: Get working copy owner WorkingCopyOwner owner = ...; // Client 1: Create shared working copy ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null); // Client 2: Retrieve shared working copy ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner); // This is the same working copy assert workingCopyForClient1 == workingCopyForClient2; // Client 1: Discard shared working copy workingCopyForClient1.discardWorkingCopy(); // Client 2: Attempt to retrieve shared working copy and find out it's null workingCopyForClient2 = originalUnit.findWorkingCopy(owner); assert workingCopyForClient2 == null;
È possibile creare una CompilationUnit da zero utilizzando i metodi factory in AST. I nomi di questi metodi iniziano con new.... Di seguito viene riportato un esempio di creazione di una classe HelloWorld.
Il primo frammento è costituito dall'output generato:
package example;
import java.util.*;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello" + " world");
}
}
Il frammento di seguito riportato rappresenta il codice corrispondente che genera l'output.
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; }Il metodo IScanner viene utilizzato per suddividere l'origine dell'input in token. Ciascun token ha un valore specifico che viene definito nell'interfaccia ITerminalSymbols. È piuttosto semplice iterare e richiamare il token giusto. Si consiglia inoltre di utilizzare lo scanner se si desidera trovare la posizione della parola chiave super in un SuperMethodInvocation.
Alcune modifiche del codice di origine non sono fornite attraverso l'API di elemento Java. Un modo più generico per modificare il codice di origine (ad esempio modificando il codice di origine per elementi esistenti) consiste nell'utilizzo del codice di origine primitivo dell'unità di compilazione e l'API di riscrittura del DOM/AST.
Per eseguire la riscrittura DOM/AST, esistono due insiemi di API: la riscrittura descrittiva e la riscrittura di modifica.
L'API descrittiva non modifica l'AST ma utilizza l'API ASTRewrite per generare le descrizioni delle modifiche.
Il rescrittore AST raccoglie le descrizioni delle modifiche ai nodi e le traduce in modifiche di testo che possono essere applicate all'origine.
// creation of a Document
ICompilationUnit cu = ... ; // content is "public class X {\n}"
String source = cu.getBuffer().getContents();
Document document= new Document(source);
// creation of DOM/AST from a ICompilationUnit
ASTParser parser = ASTParser.newParser(AST.JLS2);
parser.setSource(cu);
CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);
// creation of ASTRewrite
ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());
// description of the change
SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
SimpleName newName = astRoot.getAST().newSimpleName("Y");
rewrite.replace(oldName, newName, null);
// computation of the text edits
TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));
// computation of the new source code
edits.apply(document);
String newSource = document.get();
// update of the compilation unit
cu.getBuffer().setContents(newSource);
La modifica dell'API consente di modificare direttamente l'AST:
// creation of a Document ICompilationUnit cu = ... ; // content is "public class X {\n}" String source = cu.getBuffer().getContents(); Document document= new Document(source); // creation of DOM/AST from a ICompilationUnit ASTParser parser = ASTParser.newParser(AST.JLS2); parser.setSource(cu); CompilationUnit astRoot = (CompilationUnit) parser.createAST(null); // start record of the modifications astRoot.recordModifications(); // modify the AST TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0) SimpleName newName = astRoot.getAST().newSimpleName("Y"); typeDeclaration.setName(newName); // computation of the text edits TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true)); // computation of the new source code edits.apply(document); String newSource = document.get(); // update of the compilation unit cu.getBuffer().setContents(newSource);
Se il plugin dell'utente deve essere a conoscenza delle modifiche apportate a elementi Java, è possibile registrare un IElementChangedListener Java con JavaCore.
JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());
È possibile essere più precisi e specificare il tipo di eventi a cui si è interessati utilizzando addElementChangedListener(IElementChangedListener, int).
Ad esempio, se si è interessati solo alla ricezione degli eventi durante un'operazione di riconciliazione:
JavaCore.addElementChangedListener(new MyJavaElementChangeReporter(), ElementChangedEvent.POST_RECONCILE);
Esistono due tipi di eventi supportati da JavaCore:
I listener di modifiche degli elementi Java assomigliano concettualmente ai listener di modifiche delle risorse (descritti nella sezione relativa alla traccia delle modifiche delle risorse). Il frammento di codice seguente implementa un reporter di modifiche degli elementi Java che stampa i delta dell'elemento nella console di sistema.
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 include l'elemento che è stato modificato e gli indicatori che descrivono il tipo di modifica. Per la maggior parte del tempo, la struttura delta è collegata al livello del modello Java. I client devono quindi spostarsi in questo delta utilizzando getAffectedChildren per individuare i progetti modificati.
Il metodo di esempio di seguito riportato attraversa un delta e visualizza gli elementi aggiunti, rimossi e modificati:
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]); } }
Vari tipi di operazioni possono attivare la notifica di modifica di un elemento Java. Ecco alcuni esempi:
Allo stesso modo di IResourceDelta, è possibile eseguire il batch dei delta dell'elemento Java utilizzando un IWorkspaceRunnable. I delta che risultano da diverse operazioni del modello Java eseguite in un IWorkspaceRunnable vengono uniti e riportati contemporaneamente.
JavaCore fornisce un metodo di esecuzione per il batch delle modifiche dell'elemento Java.
Ad esempio, il frammento di codice di seguito riportato attiva 2 eventi di modifica dell'elemento Java:
// Get package IPackageFragment pkg = ...; // Create 2 compilation units ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null); ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);
Il frammento di codice di seguito riportato attiva 1 evento di modifica dell'elemento Java:
// Get package IPackageFragment pkg = ...; // Create 2 compilation units 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);