Manipulera Java-kod

Ditt insticksprogram kan använda JDT-API:t till att skapa klasser eller gränssnitt, lägga till metoder till befintliga typer eller ändra metoderna för typer.

Det enklaste sättet att ändra Java-objekt är att använda Java-element-API:t. Mer allmänna tekniker kan användas till att arbeta med den råa källkoden för ett Java-element.

Kodmodifiering med Java-element

Generera en kompileringsenhet

Det enklaste sättet att programmatiskt generera en kompileringsenhet är att använda IPackageFragment.createCompilationUnit. Du anger namnet och innehåller i kompileringsenheten. Kompileringsenheten skapas inuti paketet och den nya ICompilationUnit returneras.

Du kan skapa en kompileringsenhet generiskt genom att skapa en filresurs vars filtillägg är ".java" i den mapp som motsvarar paketkatalogen. Användning av det generiska resurs-API:t är en bakdörr till Java-verktygen. Det innebär att Java-modellen inte uppdateras förrän de generiska resursändringslyssnarna meddelas och JDT-lyssnarna uppdaterar Java-modellen med den nya kompileringsenheten.

Modifiera en kompileringsenhet

De mest enkla modifieringar av Java-källkod kan köras med Java-element-API:t.

Du kan t.ex. fråga en typ från en kompileringsenhet. När du harIType kan du använda protokoll som createField, createInitializer, createMethod, eller createType för att lägga till källkodsmedlemmar i typen. Källkoden och information om platsen för medlemmen tillhandahålls i dessa metoder.

Gränssnittet ISourceManipulation definierar vanliga källkodsmanipulationer för Java-element. Det innefattar metoder för att byta namn på, flytta, kopiera och ta bort en typs medlem.

Arbetskopior

Du kan modifiera kod genom att manipulera kompileringsenheten (vilket därigenom modifierar den underliggande IFile) eller så kan du modifiera en kopia i minnet av kompileringsenheten, kallat en arbetskopia.

En arbetskopia erhålls från en kompileringsenhet med hjälp av metoden getWorkingCopy. (Obs! Kompileringsenheten behöver inte finns i Java-modellen för att en arbetskopia ska kunna skapas.)  Vem som än skapar en sådan arbetskopia ansvarar för att den tas bort när den inte behövs längre med hjälp av metoden discardWorkingCopy.

Arbetskopior modifierar en buffert i minnet. Metoden getWorkingCopy() skapar en standardbuffert, med klienter kan tillhandahålla sig egen buffertimplementation med hjälp av metoden getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Klienter kan manipulera texten i denna buffert direkt. Om de gör det, måste de då och då synkronisera arbetskopian med bufferten med någon av metodernareconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Slutligen går det att spara en arbetskopia på disk (och ersätta den ursprungliga kompileringsenheten) med metoden commitWorkingCopy.  

Ett exempel: följande kodstycke skapar en arbetskopia på en kompileringsenhet med en anpassad ägare till arbetskopian. Stycket modifierar bufferten, stämmer av ändringarna, skickar ändringarna till disk och tar sedan slutligen bort arbetskopian.

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

Det går också att dela arbetskopior mellan flera klienter genom att använda en ägare till arbetskopian. En arbetskopia kan hämtas vid ett senare tillfälle med hjälp av metoden findWorkingCopy. En delad arbetskopia kopplas därigenom till den ursprungliga kompileringsenheten och till en ägare.

Följande kodexempel visar hur klient 1 skapar en delad arbetskopia, klient 2 hämtar den, klient 1 kastar bort den och klient 2 försöker hämta den för att upptäcka att den inte finns längre:

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

Kodmodifiering med DOM/AST-API:t

Man kan skapa en kompileringsenhet på tre sätt. Det första sättet är att använda ASTParser. Det andra är att användaICompilationUnit#reconcile(...). Det tredje är att börja från noll med fabriksmetoderna i AST (Abstract Syntax Tree).

Skapa ett AST från befintlig källkod

En förekomst av ASTParser måste skapas med ASTParser.newParser(int).

Källkoden ges till ASTParser med en av följande metoder: Sedan skapas AST med ett anrop till createAST(IProgressMonitor).

Resultatet är ett AST med korrekta källpositioner för varje nod. Upplösningen av bindningar måste begäras innan trädet skapas med setResolveBindings(boolean). Upplösning av bindningarna är en kostsam operation som bara ska göras när det är nödvändigt. Så snart som trädet har modifierats går alla positioner och bindningar förlorade.

Skapa en AST genom att stämma av en arbetskopia

Om en arbetskopia inte är konsekvent (har modifierats) kan du skapa ett AST genom att anropa metodenreconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Om du vill begära AST-skapande anropar du metoden reconcile(...) med AST.JLS2 som första parameter.

Dess bindningar beräknas bara om problembegäraren är aktiv eller om problemavkänningen är framtvingad. Upplösning av bindningarna är en kostsam operation som bara ska göras när det är nödvändigt. Så snart som trädet har modifierats går alla positioner och bindningar förlorade.

Från noll

Det går att skapa en Kompileringsenhet från noll med fabriksmetoderna i AST. Dessa metodnamn börjar med new.... Följande är ett exempel som skapar en klass med namnet HelloWorld.

Det första stycket är de genererade utdata:

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

Följande stycke är motsvarande kod som genererar utdata.

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

Hämta extra positioner

DOM/AST-noden innehåller bara ett par med positioner (startpositionen och nodens längd). Detta är inte alltid tillräckligt. Om du vill hämta mellanliggande positioner bör du användaIScanner-API:t. Ett exempel: vi har en InstanceofExpression för vilken vi vill ta reda på positionerna för operatorn instanceof. Vi kan skriva följande metod för att uppnå detta:
	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 används till att dela upp indatakällan i tokens. Varje token har ett specifikt värde som definieras i gränssnittetITerminalSymbols. Det är ganska enkelt att iterera och hämta rätt token. Vi rekommenderar även att du använder skannern om du vill leta upp positionen för nyckelordet super i enSuperMethodInvocation.

Källkodsmodifieringar

Vissa källkodsmodifieringar tillhandahålls inte via Java-element-API:t. Ett mer allmänt sätt att redigera källkod (t.ex. att ändra källkoden för befintliga elements) uppnås genom att använda kompileringsenhetens råa källkod och omskrivnings-API:t för DOM/AST.

Om du vill utföra DOM/AST-omskrivning, finns två uppsättningar API:er: den beskrivande omskrivningen och den modifierande omskrivningen.

Det beskrivande API:r modifierar inte AST men använder ASTRewrite-API:t till att generera modifieringarnas beskrivningar. AST-omskrivaren samlar in beskrivningar av modifieringarna av noder och översätter dessa beskrivningar till textredigeringar som sedan kan tillämpas på den ursprungliga källkoden.

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

Det modifierande API:T gör det möjligt att modifiera AST direkt:

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

Svara på ändringar i Java-element

Om ditt insticksprogram behöver få mer information om ändringar av Java-element i efterhand kan du registrera en Java IElementChangedListener med JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Du kan vara mer specifik och ange vilken typ av händelser du är intresserad av med addElementChangedListener(IElementChangedListener, int).

Ett exempel: om du bara är intresserad av att lyssna efter händelser under en avstämningsoperation:

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

Det finns två typer av händelser som hanteras av JavaCore:

Lyssnare av ändringar i Java-element är likartade konceptuellt till resursändringslyssnare (beskrivs i spåra resursändringar). Följande kodstycke implementerar en funktion som rapporterar ändringar av Java-element som skriver ut elementdeltan på systemkonsolen.

   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 inkluderar det element som ändrades och flaggor som beskriver vilken typ av ändring som utfördes. Den mesta tiden är deltaträdet rotat på Java-modellnivå. Klienter måste sedan navigera i detta delta med getAffectedChildren för att få redan på vilka projekt som har ändrats.

Följande exempel stegar igenom ett delta och skriver ut de element som har lagts till, tagits bort eller ändrats:

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

Det finns flera typer av operationer som kan utlösa en avisering om ändrat Java-element. Här är några exempel:

På liknande sätt somIResourceDelta kan Java-elements deltan batchbearbetas med en IWorkspaceRunnable. De deltan som uppstår från flera Java-modelloperationer som körs i en IWorkspaceRunnable sammanfogas och rapporteras samtidigt.  

JavaCore tillhandahåller metoden run för batchbearbetning av Java-elementändringar.

Ett exempel: följande kodfragment utlöser 2 händelser som ändrar Java-element:

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

Till skillnad från följande kodfragment som utlöser 1 händelse som ändrar Java-element:

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