Manipulation du code Java

Votre plug-in peut utiliser l'API JDT pour créer des classes ou des interfaces, ajouter des méthodes aux types existants ou modifier les méthodes qu'ils comportent déjà.

Le moyen le plus simple de modifier un objet Java est d'utiliser l'API de l'élément Java correspondant. Des techniques plus générales peuvent être utilisées pour travailler sur le code source brut d'un élément Java.

Modification de code à l'aide d'éléments Java

Génération d'une unité de compilation

Le moyen le plus simple de générer une unité de compilation par programme est d'utiliser IPackageFragment.createCompilationUnit. Vous spécifiez le nom et le contenu de l'unité de compilation. Cette dernière est créée à l'intérieur du package et le nouvel élément ICompilationUnit est renvoyé.

Vous pouvez créer une unité de compilation de manière générique en créant un fichier dont l'extension est ".java" dans le dossier approprié qui correspond au répertoire du package. JDT n'a pas connaissance de l'utilisation de la ressource générique, si bien que le modèle Java n'est pas mis à jour tant que les programmes d'écoute (ou "listeners") d'événements de modification ne reçoivent pas de notification et que ceux de JDT mettent à jour le modèle Java avec la nouvelle unité de compilation.

Modification d'une unité de compilation

Les modifications de code source Java les plus simples peuvent être réalisées à l'aide de l'API de l'élément Java.

Par exemple, vous pouvez demander un type à une unité de compilation. Une fois en possession du IType, vous pouvez utiliser des protocoles, tels que createField, createInitializer, createMethod ou createType pour ajouter des membres de code source au type. Le code source et les informations sur l'emplacement du membre sont fournis dans ces méthodes.

L'interface ISourceManipulation définit des manipulations de code source courantes pour les éléments Java. Elle comprend des méthodes permettant de renommer, de déplacer, de copier ou de supprimer un membre de type.

Copies d'exécution

Vous pouvez modifier le code par manipulation de l'unité de compilation (ce qui modifie le IFile sous-jacent) ou en modifiant une copie en mémoire de l'unité de compilation appelée copie de travail.

La méthode getWorkingCopy permet d'obtenir une copie de travail à partir d'une unité de compilation. Notez que l'unité de compilation n'a pas besoin d'exister dans le modèle Java pour qu'une copie de travail soit créée.Lorsqu'elle n'a plus d'utilité, cette copie de travail doit être ignorée par la personne qui l'a créée à l'aide de la méthode discardWorkingCopy.

Les copies de travail modifient un tampon en mémoire. La méthode getWorkingCopy() crée une mémoire tampon par défaut mais les clients peuvent implémenter leur propre mémoire tampon à l'aide de la méthode getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Les clients peuvent manipuler directement le contenu de cette mémoire tampon. Dans ce cas, ils doivent régulièrement synchroniser la copie de travail avec la mémoire tampon à l'aide de la méthode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Enfin, une copie de travail peut être sauvegardée sur disque (en remplacement de l'unité de compilation d'origine) à l'aide de la méthode commitWorkingCopy.  

Par exemple le fragment de code suivant crée une copie de travail sur une unité de compilation à l'aide d'un propriétaire de copie de travail personnalisée. Le fragment modifie la mémoire tampon, synchronise les modifications, valide les modifications sur disque et enfin, ignore la copie de travail.

    // Obtenir l'unité de compilation d'origine
    ICompilationUnit originalUnit = ...;
    
    // Obtenir le propriétaire de la copie de travail
    WorkingCopyOwner owner = ...;
    
    // Créer la copie de travail
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Modifier la mémoire tampon et synchroniser
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Valider les modifications
    workingCopy.commitWorkingCopy(false, null);
    
    // Détruire la copie de travail
    workingCopy.discardWorkingCopy();

Les copies de travail peuvent également être partagées par plusieurs clients utilisant un propriétaire de copie de travail. Une copie de travail peut ensuite être extraite à l'aide de la méthode findWorkingCopy. Une copie de travail partagée est donc indexée sur l'unité de compilation d'origine et sur un propriétaire de copie de travail.

L'exemple ci-dessous montre comment Client 1 crée une copie partagée, Client 2 extrait cette copie de travail, Client 1 ignore la copie de travail et Client 2, qui tente d'extraire la copie partagée, constate qu'elle n'existe plus :

    // Client 1 & 2 : Obtenir l'unité de compilation d'origine
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2 : Obtenir le propriétaire de la copie de travail
    WorkingCopyOwner owner = ...;
    
    // Client 1 : Créer la copie de travail partagée
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2 : Obtenir la copie de travail partagée
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Il s'agit de la même copie de travail partagée
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1 : Ignorer la copie de travail partagée
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2 : Tenter d'obtenir la copie de travail partagée et constater son absence
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Modification de code à l'aide de l'API DOM/AST

Vous pouvez créer une unité de compilation CompilationUnit de trois manières différentes. La première utilise ASTParser. La seconde utilise ICompilationUnit#reconcile(...). Tandis que la troisième, crée intégralement l'unité à l'aide des méthodes de fabrique sur l'arbre de syntaxe abstraite AST (Abstract Syntax Tree).

Création d'un élément AST à partir d'un code source existant

Une instance de ASTParser doit être créée avec ASTParser.newParser(int).

Le code source est donné à l'élément ASTParser avec une des méthodes suivantes : L'élément AST est créé par l'appel de createAST(IProgressMonitor).

Un élément AST avec des emplacements source corrects est généré pour chaque noeud. La résolution des liaisons doit être demandée avant la création de l'arborescence avec setResolveBindings(boolean). La résolution des liaisons est une opération coûteuse qui ne doit être exécutée qu'en cas de nécessité absolue. Dès que l'arborescence est modifiée, toutes les positions et liaisons sont perdues.

Création d'un élément AST par la réconciliation d'une copie de travail

Si une copie de travail n'est pas cohérente (a été modifiée), il est alors possible de créer un élément AST en appelant la méthode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Pour demander la création d'un élément AST, appelez la méthode reconcile(...) avec AST.JLS2 en tant que premier caractère.

Ses liaisons sont calculées uniquement si le programme de demande d'incidents est actif ou si la détection des incidents a été provoquée. La résolution des liaisons est une opération coûteuse qui ne doit être exécutée qu'en cas de nécessité absolue. Dès que l'arborescence est modifiée, toutes les positions et liaisons sont perdues.

Création à partir de rien

Vous pouvez créer intégralement une CompilationUnit à l'aide des méthodes de fabrique sur AST. Le noms de ces méthodes commencent par new.... Voici un exemple de création d'une classe HelloWorld.

Le premier fragment de code est la sortie générée :

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

Le fragment qui suit est le code correspondant de génération de la sortie.

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

Extraction de positions supplémentaires

Le noeud DOM/AST contient uniquement deux positions (la position de début et la longueur du noeud). Ces valeurs ne suffisent pas toujours. Pour extraire des positions intermédiaires, vous devez utiliser l'API IScanner. Par exemple, si nous voulons connaître les positions de l'opérateur instanceof d'un élément InstanceofExpression, nous pouvons rédiger la méthode suivante :
	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;
	}
IScanner est utilisé pour diviser la source d'entrée en jetons. Chaque jeton à une valeur spécifique définie dans l'interface ITerminalSymbols. Il suffit ensuite de parcourir les jetons et d'extraire celui qui convient. Nous vous recommandons également d'utiliser le scanneur pour trouver la position du mot clé super dans un SuperMethodInvocation.

Modifications du code source

Certaines modifications de code source ne sont pas réalisables via l'API de l'élément Java. Il existe des techniques d'édition plus générales (par exemple, pour modifier le code source d'éléments existants) qui consistent à utiliser le code source brut et l'API de réécriture de DOM/AST.

Pour effectuer la réécriture DOM/AST, il existe deux ensembles d'API : la réécriture descriptive et la réécriture de modification.

La réécriture descriptive ne modifie pas l'AST mais utilise l'API ASTRewrite pour générer les descriptions des modifications. Le programme de réécriture AST rassemble les descriptions des modifications apportées aux noeud et les convertit en modifications de texte pouvant être appliquées à la source d'origine.

   // création d'un document
   ICompilationUnit cu = ... ; // le contenu est "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // création d'un élément DOM/AST à partir d'une unité ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // création d'un élément ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // description de la modification
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // calcul des modifications de texte
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // calcul du nouveau code source
   edits.apply(document);
   String newSource = document.get();

   // mise à jour de l'unité de compilation
   cu.getBuffer().setContents(newSource);

La modification de l'API permet de modifier directement l'AST :

   // création d'un document
   ICompilationUnit cu = ... ; // le contenu est "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // création d'un élément DOM/AST à partir d'une unité ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // démarrage de l'enregistrement des modifications
   astRoot.recordModifications();

   // modification de l'AST
   TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0)
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   typeDeclaration.setName(newName);

   // calcul des modifications de texte
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // calcul du nouveau code source
   edits.apply(document);
   String newSource = document.get();

   // mise à jour de l'unité de compilation
   cu.getBuffer().setContents(newSource);

Réponse aux modifications dans les éléments Java

Si votre plug-in doit avoir connaissance des modifications apportées aux éléments Java une fois que ces dernières ont eu lieu, vous pouvez enregistrer un IElementChangedListener avec JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Vous pouvez être plus précis et indiquer le type d'événement qui vous intéresse à l'aide de addElementChangedListener(IElementChangedListener, int).

Par exemple, si seuls les événements qui se produisent lors d'une opération de réconciliation vous intéressent, utilisez :

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

JavaCore prend en charge deux types d'événements :

D'un point de vue conceptuel, les programmes d'écoute de modifications d'éléments Java sont similaires aux programmes d'écoute de modifications de ressources (décrits dans la section Suivi des modifications de ressources). Le fragment de code suivant implémente un "rapporteur" de modification d'élément Java qui imprime les deltas de l'élément sur la console système.

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

L'objet IJavaElementDelta inclut l'élément qui a été modifié ainsi que des indicateurs décrivant le type de modification qu'il a subi. La plupart du temps, l'arborescence du delta est localisée au niveau du modèle Java. Les clients doivent alors naviguer dans ce delta avec getAffectedChildren pour trouver quels projets ont été modifiés.

L'exemple de méthode ci-dessous traverse une arborescence de deltas et imprime les éléments ajoutés, supprimés et modifiés :

    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");
                }
                /* D'autres indicateurs peuvent aussi être vérifiés */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Plusieurs types d'opérations peuvent déclencher une notification de modification d'élément Java, notamment :

Comme pour IResourceDelta les deltas de l'élément Java peuvent être regroupés en lot à l'aide de IWorkspaceRunnable. Les deltas issus de plusieurs modèles Java exécutés dans le cadre d'un IWorkspaceRunnable sont fusionnés et signalés en même temps.  

JavaCore fournit une méthode d'exécution pour les modifications d'élément Java par lots.

Par exemple, le fragment de code suivant déclenche 2 événements de modification d'élément Java :

    // Obtenir le package
    IPackageFragment pkg = ...;
    
    // Créer 2 unités de compilation
    	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
    	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Tandis que le fragment de code suivant déclenche un événement de modification d'élément Java :

    // Obtenir le package
    IPackageFragment pkg = ...;
    
    // Créer 2 unités de compilation
    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);