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 sorgente primitivo di un elemento Java.

Modifica del codice mediante elementi Java

Creazione di un'unità di compilazione

Il modo più semplice per creare 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à di compilazione viene creata all'interno del pacchetto, quindi viene restituito il nuovo elemento 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 dell'API di risorse generiche è alla base della strumentazione Java,per cui il modello Java non viene aggiornato fino alla notifica dei listener di modifica delle risorse generiche e fino a quando il modello Java non viene aggiornato dai listener JDT con la nuova unità di compilazione.

Modifica di un'unità di compilazione

Le modifiche più semplici al codice sorgente Java possono essere apportate utilizzando l'API di elemento Java.

Ad esempio, è possibile eseguire la query di un tipo da un'unità di compilazione. Dopo aver ottenuto l'interfaccia IType, è possibile utilizzare i protocolli come createField, createInitializer, createMethod, oppure createType per aggiungere membri di codice sorgente al tipo. Il codice sorgente e le informazioni relative alla posizione del membro vengono forniti in questi metodi.

L'interfaccia ISourceManipulation definisce le modifiche dell'origine comune per gli elementi Java. Sono compresi metodi per la ridenominazione, lo spostamento, la copia o l'eliminazione del membro di un tipo.

Copie di lavoro

Per cambiare il codice è possibile modificare l'unità di compilazione(in questo modo l'interfaccia IFile viene modificata) oppure una copia in memoria dell'unità di compilazione denominata copia di lavoro.

Una copia di lavoro si ottiene da un'unità di compilazione mediante il metodo getWorkingCopy. Chiunque crei una tale copia di lavoro è responsabile della relativa distruzione quando non è più necessaria mediante il metodo destroy.

Le copie di lavoro modificano un buffer in memoria. Anche se il metodo getWorkingCopy() crea un buffer predefinito, i client possono fornire la propria implementazione del buffer mediante il metodo getWorkingCopy(IProgressMonitor, IBufferFactory, IProblemRequestor). I client possono modificare il testo di questo buffer direttamente. In tal caso, essi devono sincronizzare la copia di lavoro con il buffer volta per volta mediante il metodo reconcile() o il metodo reconcile(boolean,IProgressMonitor).

Infine, è possibile salvare una copia di lavoro su disco (sostituendo l'unità di compilazione originale) mediante il metodo commit.

Ad esempio il frammento di codice crea una copia di lavoro su un'unità di compilazione mediante una factory del buffer personalizzata. Il frammento modifica il buffer, riconcilia le modifiche, ne esegue il commit su disco e infine distrugge la copia di lavoro.

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

E' inoltre possibile che le copie di lavoro siano condivise da diversi client. Una copia di lavoro condivisa viene creata mediante il metodo getSharedWorkingCopy e può essere recuperata successivamente mediante il metodo findSharedWorkingCopy. Una copia di lavoro condivisa viene in questo modo basata su chiavi su un'unità di compilazione originale e su una factory di buffer.

Il codice seguente mostra in che modo il client 1 crea una copia di lavoro condivisa, il client 2 la recupera, il client 1 distrugge la copia di lavoro e il client 2 che tenta di recuperare la copia di lavoro condivisa nota che questa non è più presente:

    // Client 1 & 2: Ottenere l'unità di compilazione originale
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Ottenere il factory del buffer
    IBufferFactory factory = ...;
    
    // Client 1: Creare copia di lavoro condivisa
    IWorkingCopy workingCopyForClient1 = originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Client 2: Recuperare copia di lavoro condivisa
    IWorkingCopy workingCopyForClient2 = originalUnit.findSharedWorkingCopy(factory);
     
    // Questa è la stessa copia di lavoro
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Distruggere la copia di lavoro condivisa
    workingCopyForClient1.destroy();
    
    // Client 2: Tentare di recuperare la copia di lavoro condivisa e individuare che sia null
    workingCopyForClient2 = originalUnit.findSharedWorkingCopy(factory);
    assert workingCopyForClient2 == null;

Modifica del codice mediante l'API DOM/AST

Esistono due modi per creare una CompilationUnit. Il primo consiste nell'utilizzo di un'unità di compilazione esistente. Il secondo prevede un inizio da zero utilizzando i metodi di factory su AST (Abstract Syntax Tree).

Creazione di un AST da un'unità di compilazione esistente

Si ottiene con i metodi di analisi su AST: Tutti questi metodi imposteranno in maniera appropriata le posizioni di ciascun nodo della struttura risultante. La risoluzione dei binding deve essere richiesta prima della struttura. La risoluzione dei binding è un'operazione costosa e deve essere effettuata solo quando è necessario. Non appena la struttura è stata modificata, tutte le posizioni e i binding vengono persi.

Da zero

E' possibile creare una CompilationUnit da zero mediante i metodi di factory su AST. Tali metodi denominano l'inizio con new.... Di seguito è riportato un esempio che crea una classe HelloWorld.

Il primo frammento è l'output generato:

	package example;
import java.util.*;
public class HelloWorld {
public static void main(String[] args) {
			System.out.println("Hello" + " world");
		}
	}

Il seguente frammento è 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);

Recupero di ulteriori posizioni

Il nodo DOM/AST contiene solo un paio di posizioni (la posizione di partenza e la lunghezza del nodo). Ciò non è sempre sufficiente. Per recuperare le posizioni intermedie, occorre utilizzare l'API IScanner API. Ad esempio, è presente una InstanceofExpression per la quale si desidera conoscere le posizione dell'operatore instanceof. Per questa operazione è possibile scrive il metodo seguente:
	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;
	}
L'interfaccia IScanner viene utilizzata per dividere l'input in token. Ogni token presenta un valore specifico definito nell'interfaccia ITerminalSymbols. E' abbastanza semplice esplorare e recuperare il token giusto. Si consiglia di utilizzare lo scanner per ricercare la posizione della parola chiave super in SuperMethodInvocation.

Modifiche generiche del codice sorgente

Alcune modifiche del codice sorgente non sono fornite attraverso l'API di elemento Java. Un modo più generico per modificare il codice sorgente (ad esempio modificando il codice sorgente di elementi esistenti) consiste nell'utilizzo del codice sorgente 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 sorgente 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();

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 pertanto le tecniche utilizzate dal JDT per analizzare il codice in strutture programmatiche non sono correntemente 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());

E' possibile specificare ulteriormente il tipo di eventi a cui si è interessati utilizzando addElementChangedListener(IElementChangedListener, int).

Ad esempio, se si è interessati solo al listening di eventi prima che i generatori vengano eseguiti:

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

Esistono tre tipi di elementi 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 comprende element che era stato modificato e flags che descrive il tipo di modifica apportata. La maggior parte delle volete la struttura delta deriva dal livello del modello Java. I client devono quindi esplorare tale delta utilizzando getAffectedChildren per individuare i progetti modificati.

Il metodo dell'esempio che segue attraversa un delta e restituisce 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]);
        }
    }

Diversi tipi di operazioni possono attivare la notifica della modifica di un elemento Java. Di seguito sono riportati alcuni esempi:

Come per IResourceDelta i delta dell'elemento Java possono essere inseriti in batch mediante un'interfaccia IWorkspaceRunnable. I delta risultanti da diverse operazioni del modello Java eseguite all'interno di un'interfaccia IWorkspaceRunnable vengono riuniti e riportati nello stesso momento.

Ad esempio il seguente frammento attiverà 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);

Invece l'esempio seguente attiverà 1 evento di modifica dell'elemento Java:

    // Ottenere il pacchetto
    IPackageFragment pkg = ...;
    
    // Creare 2 unità di compilazione
    IWorkspace workspace = ResourcesPlugin.getWorkspace();
    workspace.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, 2002. Tutti i diritti riservati.