Modifica del codice Java

Il plug-in 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 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 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 origine. Il codice origine e le informazioni relative al percorso del membro vengono forniti in questi metodi.

L'interfaccia ISourceManipulation definisce modifiche comuni del codice 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 distruzione quando la copia di lavoro non è più necessaria, mediante il metodo destroy.

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(IProgressMonitor, IBufferFactory, IProblemRequestor). 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() o il metodo reconcile(boolean,IProgressMonitor).

Infine, una copia di lavoro può essere salvata su disco (sostituendo l'unità di compilazione originale) utilizzando il metodo commit.   

Ad esempio, il frammento di codice di seguito riportato crea una copia di lavoro sull'unità di compilazione mediante un factory di buffer personalizzato. Il frammento modifica il buffer, riconcilia le modifiche, sincronizza le modifiche sul disco e infine distrugge la copia di lavoro.

    // Richiamare l'unità di compilazione originale
    ICompilationUnit originalUnit = ...;
    
    // Ottenere il factory del buffer
    IBufferFactory factory = ...;
    
    // Creare la copia di lavoro
    IWorkingCopy workingCopy = (IWorkingCopy)originalUnit.getWorkingCopy(null, factory, null);
    
    // Modificare il buffer e riconciliare
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile();
    
    // Sincronizzare le modifiche
    workingCopy.commit(false, null);
    
    // Distruggere la copia di lavoro
    workingCopy.destroy();

Le copie di lavoro possono essere inoltre condivise da vari client. Una copia di lavoro condivisa viene creata mediante il metodo getSharedWorkingCopy e può essere richiamata successivamente mediante il metodo findSharedWorkingCopy. Una copia di lavoro condivisa viene quindi codificata sull'unità di compilazione originale e su un factory di buffer.

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 distrugga 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: Richiamare l'unità di compilazione originale
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Richiamare il factory del buffer
    IBufferFactory factory = ...;
    
    // Client 1: Creare una copia di lavoro condivisa
    IWorkingCopy workingCopyForClient1 = (IWorkingCopy)originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Client 2: Richiamare la copia di lavoro condivisa
    IWorkingCopy workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
     
    // Copia di lavoro condivisa
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Distruggere la copia di lavoro condivisa
    workingCopyForClient1.destroy();
    
    // Client 2: Tentare di richiamare la copia di lavoro condivisa e individuare un valore null
    workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
    assert workingCopyForClient2 == null;

Modifica del codice mediante l'API DOM/AST

Esistono tre modi per creare una CompilationUnit. Il primo consiste nell'utilizzare un'unità di compilazione esistente. Il secondo consiste nell'utilizzare un file di classe esistente. Il terzo consiste nel partire da zero utilizzando i metodi factory in AST (Abstract Syntax Tree).

Creazione di un AST dall'unità di compilazione esistente

I metodi di analisi su AST consentono i seguenti risultati: Tutti questi metodi imposteranno correttamente le posizioni di ciascun nodo nella struttura risultante. Prima della creazione della struttura, deve essere richiesta la risoluzione dei binding. 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 da un file di classe esistente

Il metodo di analisi su AST consente i seguenti risultati: Questo metodo imposterà correttamente le posizioni di ciascun nodo nella struttura risultante. Prima della creazione della struttura, deve essere richiesta la risoluzione dei binding. 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 generiche del codice origine

Alcune modifiche del codice origine non sono fornite attraverso l'API di elemento Java. Un modo più generico per modificare il codice origine (ad esempio modificando il codice origine di elementi esistenti) consiste nell'utilizzo del codice origine primitivo dell'unità di compilazione e del DOM Java.

Tali tecniche comprendono quanto segue:

   // ottenere il codice sorgente per un'unità di compilazione
   String contents = myCompilationUnit.getBuffer().getContents();

   // Creare un JDOM modificabile
   myJDOM = new DOMFactory();
   myDOMCompilationUnit = myJDOM.createCompilationUnit(contents, "MyClass");

   // Esplorare e modificare la struttura dell'unità di compilazione utilizzando
   // il protocollo dei nodi JDOM.
   ...
   // Una volta apportate le modifiche a tutti i nodi
   // richiamare il codice origine dal nodo DOM dell'unità di compilazione.
   String newContents = myDOMCompilationUnit.getContents();

   // Impostare nuovamente questo codice nell'elemento unità di compilazione
   myCompilationUnit.getBuffer().setContents(newContents);

   // Salvare il buffer in un file.
   myCompilationUnit.save(null, false);

Questa tecnica potrebbe determinare l'associazione di indicatori di problemi a numeri di riga non corretti dato che gli elementi Java non sono stati aggiornati direttamente.

Il modello di elemento Java non differisce troppo dai metodi e dai campi. La struttura di sintassi astratta utilizzata dal compilatore non è disponibile come API, quindi le tecniche utilizzate da JDT per esaminare l'origine in strutture programmatiche non sono attualmente disponibili come API.

Risposta alle modifiche in elementi Java

Se il plug-in 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 prima dell'esecuzione dei generatori:

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter(), ElementChangedEvent.PRE_AUTO_BUILD);

Esistono tre 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");
                }
                /* Possono essere controllati anche altri indicatori */
                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:

    // Ottenere il pacchetto
    IPackageFragment pkg = ...;
    
    // Creare 2 unità di compilazione
    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:

    // Ottenere il pacchetto
    IPackageFragment pkg = ...;
    
    // Creare 2 unità di compilazione
    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);	

Copyright IBM Corporation e altri 2000, 2003. Tutti i diritti riservati.