Modifica del codice Java

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.

Modifica del codice mediante elementi Java

Generazione di un'unità di compilazione

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.

Modifica di un'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.

Copie di lavoro

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;

Modifica del codice mediante l'API DOM/AST

Esistono tre modi per creare una CompilationUnit. Il primo consiste nell'utilizzare ASTParser. Il secondo consiste nell'utilizzareICompilationUnit#reconcile(...). Il terzo consiste nel partire da zero utilizzando i metodi factory in AST (Abstract Syntax Tree).

Creazione di un AST da un codice di origine esistente

È necessario creare un'istanza di ASTParser con ASTParser.newParser(int).

Il codice di origine viene assegnato al ASTParser con uno dei seguenti metodi: L'AST viene quindi creato richiamando createAST(IProgressMonitor).

Il risultato è un AST con percorsi di origine corretti per ciascun nodo. Deve essere richiesta la risoluzione dei binding, prima della creazione della struttura ad albero con setResolveBindings(boolean). La risoluzione dei binding è un'operazione costosa e deve essere eseguita solo quando necessario. Non appena la struttura viene modificata, tutte le posizioni e i binding vengono perduti.

Creazione di un AST riconciliando una copia di lavoro

Se una copia di lavoro non è congruente (è stata modificata), è possibile creare un AST richiamando il metodo reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Per richiedere la creazione di un AST, richiamare il metodo reconcile(...) con AST.JLS2 come primo parametro.

I binding vengono elaborati solo se il richiedente del problema è attivo oppure se viene forzata l'individuazione del problema. La risoluzione dei binding è un'operazione costosa e deve essere eseguita solo quando necessario. Non appena la struttura viene modificata, tutte le posizioni e i binding vengono perduti.

Da zero

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

Richiamo di posizioni supplementari

Il nodo DOM/AST contiene solo una coppia di posizioni (la posizione iniziale e la lunghezza del nodo). Ciò non è sempre sufficiente. Per richiamare le posizioni intermedie, deve essere utilizzata l'API IScanner. Ad esempio, si supponga di disporre di una InstanceofExpression per la quale si desidera conoscere le posizioni dell'operatore instanceof. È possibile scrivere il metodo di seguito riportato per raggiungere questi risultati:
	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.

Modifiche del codice di origine

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

Risposta alle modifiche in elementi Java

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