Java-code manipuleren

U kunt de API van JDT gebruiken voor plugin om klassen of interfaces te maken, methoden aan bestaande typen toe te voegen, of methoden voor typen te wijzigen.

U kunt Java-objecten het gemakkelijkst wijzigen met de API voor Java-elementen. Daarnaast zijn er enkele algemene methoden om de 'ruwe' broncode van een Java-element te bewerken.

Code wijzigen met Java-elementen

Een compilatie-eenheid genereren

U kunt eenvoudig programmatisch een compilatie-eenheid genereren met IPackageFragment.createCompilationUnit. U geeft de naam en inhoud van de compilatie-eenheid op. De compilatie-eenheid wordt gemaakt in het pakket en de nieuwe ICompilationUnit wordt geretourneerd.

Een algemene methode om een compilatie-eenheid te maken is een bestandsresource met de extensie ".java" te maken in de juiste map van de bijbehorende pakketdirectory. De generieke resource-API dient slechts te worden gebruikt als alternatief voor de Java-tools, want het Java-model wordt niet bijgewerkt totdat de generieke resourcewijzigingslisteners worden geïnitieerd en het Java-model door de JDT-listeners wordt bijgewerkt met de nieuwe compilatie-eenheid.

Een compilatie-eenheid wijzigen

Veel eenvoudige Java-broncodewijzigingen kunnen worden aangebracht met behulp van de API voor Java-elementen.

U kunt bijvoorbeeld een type van een compilatie-eenheid opvragen. Als u het IType hebt, kunt u protocollen zoals createField, createInitializer, createMethod of createType gebruiken om broncodeleden aan het type toe te voegen. De broncode en gegevens over de locatie van het lid worden in deze methoden aangeleverd.

Met de interface ISourceManipulation bevat definities voor veelgebruikte bronbewerkingen van Java-elementen. Hieronder vallen methoden voor het hernoemen, verplaatsen, kopiëren of verwijderen van elementen van een type.

Werkexemplaren

Code kan worden gewijzigd door de compilatie-eenheid rechtstreeks te bewerken (hierbij wordt de onderliggende IFile gewijzigd) of door een exemplaar van de compilatie-eenheid in het geheugen te bewerken. Het laatste wordt een werkexemplaar genoemd.

Een werkexemplaar van een compilatie-eenheid wordt verkregen via de methode getWorkingCopy. (Opmerking: de compilatie-eenheid hoeft niet in het Java-model aanwezig te zijn om een werkexemplaar te maken.) Degene die het werkexemplaar maakt, moet het ook weer wissen met de methode discardWorkingCopy als het werkexemplaar niet meer nodig is.

Werkexemplaren worden bewerkt in een geheugenbuffer. Via de methode getWorkingCopy() wordt een standaardbuffer gemaakt, maar clients kunnen hun eigen bufferimplementatie aanleveren met behulp van de methode getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Clients kunnen de tekst in deze buffer rechtstreeks bewerken. Hierbij moeten zij het werkexemplaar tezijnertijd met de buffer synchroniseren met behulp van de methode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Ten slotte kan het werkexemplaar op schijf worden opgeslagen (hierbij wordt de oorspronkelijke compilatie-eenheid vervangen) met de methode commitWorkingCopy.  

Zo wordt met de volgende code een werkexemplaar van een compilatie-eenheid gemaakt met de eigenaar van een aangepast werkexemplaar. In de code wordt eerst de buffer gewijzigd. Vervolgens worden de wijzigingen gesynchroniseerd en op schijf opgeslagen. Ten slotte wordt het werkexemplaar weer gewist.

    // Oorspronkelijke compilatie-eenheid ophalen
    ICompilationUnit originalUnit = ...;
    
    // Eigenaar van werkexemplaar ophalen
    WorkingCopyOwner owner = ...;
    
    // Werkexemplaar maken
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Buffer wijzigen en synchroniseren
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Wijzigingen vastleggen
    workingCopy.commitWorkingCopy(false, null);
    
    // Werkexemplaar wissen
    workingCopy.discardWorkingCopy();

Werkexemplaren kunnen ook gedeeld worden gebruikt door verschillende clients met behulp van een werkexemplaareigenaar. Een werkexemplaar kan later worden opgehaald met de methode findWorkingCopy. Een gedeeld werkexemplaar is aldus gekoppeld aan de oorspronkelijke compilatie-eenheid en een werkexemplaareigenaar.

In het onderstaande voorbeeld ziet u hoe client 1 een gedeeld werkexemplaar maakt, client 2 dit werkexemplaar ophaalt, client 1 het werkexemplaar wist, waarna client 2 het gedeelde werkexemplaar probeert op te halen en merkt dat het niet meer bestaat:

    // Client 1 & 2: oorspronkelijke compilatie-eenheid ophalen
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: werkexemplaareigenaar ophalen
    WorkingCopyOwner owner = ...;
    
    // Client 1: Gedeeld werkexemplaar maken
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2: Gedeeld werkexemplaar ophalen
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Dit is hetzelfde werkexemplaar
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Gedeeld werkexemplaar wissen
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2: Mislukte poging om gedeeld werkexemplaar op te halen (werkexemplaar = null)
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Code wijzigen met DOM/AST-API

Er zijn drie manieren om een compilatie-eenheid te maken. 1. Met behulp van ASTParser (AST-parser). 2. Met behulp van ICompilationUnit#reconcile(...). 3. Met behulp van de factorymethoden voor AST (Abstract Syntax Tree).

Een AST maken op basis van bestaande broncode

Een instance van ASTParser moet worden gemaakt met ASTParser.newParser(int).

De broncode wordt doorgegeven aan de ASTParser via een van de volgende methoden: Vervolgens maakt u de AST door createAST(IProgressMonitor) aan te roepen.

Het resultaat is een AST met de juiste bronposities voor alle knooppunten. De omzetting van bindingen moet worden aangevraagd voordat de boomstructuur wordt gemaakt met setResolveBindings(boolean). Bindingen omzetten is een tijdrovende bewerking en moet alleen worden uitgevoerd als het echt nodig is. Zodra de structuur is gewijzigd, gaan alle posities en bindingen verloren.

Een AST maken door een werkexemplaar te synchroniseren

Als een werkexemplaar niet consistent (gewijzigd) is, kunt u een AST maken door de methode reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) aan te roepen. Om een AST aan te maken roept u de methode reconcile(...) aan met AST.JLS2 als eerste parameter.

De bindingen worden alleen berekend als de probleemopvrager actief is of als de probleemdetectie geforceerd plaatsvindt. Bindingen omzetten is een tijdrovende bewerking en moet alleen worden uitgevoerd als het echt nodig is. Zodra de structuur is gewijzigd, gaan alle posities en bindingen verloren.

Een nieuwe compilatie-eenheid maken

U kunt ook een nieuwe Compilatie-eenheid maken met de factorymethoden voor AST. De methodenamen beginnen met new.... Hieronder ziet u een voorbeeld voor de aanmaak van de klasse HelloWorld.

Het eerste stukje code is de gegenereerde uitvoer:

	voorbeeldpakket;
	import java.util.*;
	public class HelloWorld {
		public static void main(String[] args) {
			System.out.println("Hallo" + " wereld");
		}
	}

Het volgende stuk is de bijbehorende code waarmee de uitvoer wordt gegenereerd.

		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("Hallo");
		infixExpression.setLeftOperand(literal);
		literal = ast.newStringLiteral();
		literal.setLiteralValue(" wereld");
		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);

Extra posities ophalen

Het DOM/AST-knooppunt bevat slechts twee posities (de beginpositie en de lengte van het knooppunt). Dit is niet altijd voldoende. Om tussenliggende posities op te halen, moet u de API IScanner gebruiken. Stel, u hebt een InstanceofExpression waarvan u de posities van de operator instanceof wilt weten. Hiertoe schrijft u de volgende methode:
	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 wordt gebruikt om de invoerbron in tokens te verdelen. Elk token heeft een specifieke waarde die is gedefinieerd in de interface ITerminalSymbols. Het is relatief eenvoudig om het juiste token te herhalen en op te halen. Het is raadzaam de scanner te gebruiken als u de positie van het sleutelwoord super in een instantie van SuperMethodInvocation zoekt.

Broncodewijzigingen

Sommige broncodewijzigingen kunnen niet worden uitgevoerd via de API voor Java-elementen. In plaats daarvan kunt u ook (bijvoorbeeld als u de broncode van bestaande elementen wilt wijzigen) gebruik maken van de 'ruwe' broncode van de compilatie-eenheid en de herschrijf-API voor de DOM/AST.

Er zijn twee API's om de DOM/AST te herschrijven: de beschrijvings-API en de wijzigings-API.

Met de beschrijvings-API wordt de AST niet gewijzigd, maar wordt de API ASTRewrite gebruikt om de beschrijvingen van wijzigingen te genereren. De beschrijvingen van wijzigingen in knooppunten worden door de AST-rewriter verzameld en vertaald naar tekstbewerkingen, die vervolgens kunnen worden toegepast op de oorspronkelijke bron.

   // Document maken
   ICompilationUnit cu = ... ; // Inhoud is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // DOM/AST maken van ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // ASTRewrite maken
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

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

   // Tekstbewerkingen berekenen
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // Nieuwe broncode berekenen
   edits.apply(document);
   String newSource = document.get();

   // Compilatie-eenheid bijwerken
   cu.getBuffer().setContents(newSource);

Met de wijzigings-API kunt u de AST rechtstreeks bewerken:

   // Document maken
   ICompilationUnit cu = ... ; // Inhoud is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // DOM/AST maken van ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // Vastleggen van de wijzigingen beginnen
   astRoot.recordModifications();

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

   // Tekstbewerkingen berekenen
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // Nieuwe broncode berekenen
   edits.apply(document);
   String newSource = document.get();

   // Compilatie-eenheid bijwerken
   cu.getBuffer().setContents(newSource);

Reageren op wijzigingen in Java-elementen

Als uw plugin achteraf moet worden bijgewerkt met wijzigingen in Java-elementen, kunt u een Java-listener IElementChangedListener registreren met JavaCore.

JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

U kunt nog specifieker zijn en het type events opgeven waarin u bent geïnteresseerd met addElementChangedListener(IElementChangedListener, int).

Als u bijvoorbeeld alleen wilt luisteren naar events tijdens een synchronisatiebewerking, gebruikt u:

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

Er zijn twee soorten events die worden ondersteund door JavaCore:

Listeners voor wijzigingen in Java-elementen zijn vergelijkbaar met listeners voor resourcewijzigingen (zie resourcewijzigingen traceren). Met het volgende stuk code implementeert u een Java-elementwijzigingsreporter om de elementdelta's uit te voeren naar de systeemconsole.

   public class MyJavaElementChangeReporter implements IElementChangedListener {
      public void elementChanged(ElementChangedEvent event) {
         IJavaElementDelta delta= event.getDelta();
         if (delta != null) {
            System.out.println("delta ontvangen: ");
            System.out.print(delta);
         }
      }
   }

IJavaElementDelta bevat het gewijzigde element en vlaggen waarmee het soort aangebrachte wijziging wordt aangegeven. In de meeste gevallen wordt de deltastructuur toegepast op het niveau van het Java-model. Clients moeten door deze deltastructuur navigeren met getAffectedChildren om uit te zoeken welke projecten zijn gewijzigd.

In het onderstaande voorbeeld wordt een delta onderzocht en de toegevoegde, verwijderde en gewijzigde elementen afgedrukt:

    void traverseAndPrint(IJavaElementDelta delta) {
         switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " is toegevoegd");
      break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " is verwijderd");
      break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " is gewijzigd");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("De onderliggende elementen zijn gewijzigd");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("De inhoud is gewijzigd");
                }
                /* Andere vlaggen zijn ook mogelijk */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Een Java-elementwijzigingsmelding kan door verschillende bewerkingen worden geïnitieerd. Hieronder ziet u enkele voorbeelden:

Net als bij IResourceDelta kunnen de Java-elementdelta's groepsgewijs worden verwerkt met IWorkspaceRunnable. De delta's die ontstaan door verschillende Java-modelbewerkingen die in een IWorkspaceRunnable worden uitgevoerd, worden samengevoegd en tegelijk gemeld.  

JavaCore biedt een runmethode voor groepsgewijze verwerking van Java-elementwijzigingen.

Zo worden met het volgende voorbeeldcodefragment twee Java-elementwijzigingsevents geïnitieerd:

    // Pakket ophalen
    IPackageFragment pkg = ...;
    
    // Twee compilatie-eenheden maken
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Met het volgende codefragment wordt echter één Java-elementwijzigingsevent geïnitieerd:

    // Pakket ophalen
    IPackageFragment pkg = ...;
    
    // Twee compilatie-eenheden maken
    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);