Manipulér Java-kode

Din plugin kan bruge JDT API'et til at oprette klasser eller grænseflader, tilføje metoder til eksisterende typer, eller ændre typernes metoder.

Den enkleste måde at ændre Java-objekter er at bruge Java-elementets API. Flere generelle teknikker kan anvendes til at arbejde med den ubehandlede kildekode til et Java-element.

Kodeændring ved hjælp af Java-elementer

Generér en kompileringsenhed

Den nemmeste måde at generere en kompileringsenhed programmatisk er at bruge IPackageFragment.createCompilationUnit. Du angiver kompileringsenhedens navn og indhold. Kompileringsenheden oprettes i pakken og den nye ICompilationUnit returneres.

En kompileringsenhed kan oprettes generisk, ved at du opretter en filressource med filtypen ".java" i den relevante folder, som svarer til pakkebiblioteket. Anvendelsen af den generiske ressources API er som en bagdør til Java-værktøjerne, så Java-modellen opdateres ikke, før lytterne efter ændringer af den generiske ressource får besked, og JDT-lytterne opdaterer Java-modellen med den nye kompileringsenhed.

Revidér en kompileringsenhed

De enkleste Java-kildeændringer kan foretages via Java-elementets API.

Du kan f.eks. forespørge om en type fra en kompileringsenhed. Når du har IType, kan du bruge protokoller som f.eks. createField, createInitializer, createMethod eller createType til at tilføje kildekodemedlemmer til typen. Kildekoden og oplysninger om placeringen af medlemmet findes i disse metoder.

Grænsefladen ISourceManipulation definerer generelle kildemanipulationer for Java-elementer, herunder metoder til at omdøbe, flytte, kopiere eller slette en types medlem.

Arbejdskopier

Kode kan ændres ved at manipulere kompileringsenheden (og sådan ændres også den underliggende IFile), eller du kan ændre en kopi i hukommelsen af kompileringsenheden, som kaldes en arbejdskopi.

En arbejdskopi hentes fra en kompileringsenhed ved hjælp af metoden getWorkingCopy. Bemærk, at kompileringsenheden ikke nødvendigvis skal være i Java-modellen, for at arbejdskopien skal kunne oprettes. Den, som opretter en sådan arbejdskopi, er ansvarlig for at slette den, når der ikke længere er brug for den, ved hjælp af metoden discardWorkingCopy.

Arbejdskopier ændrer en buffer i hukommelsen. Med metoden getWorkingCopy() oprettes en standardbuffer, men klienter kan stille deres egen bufferimplementering til rådighed ved hjælp af metoden getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Klienter kan manipulere teksten i denne buffer direkte. Hvis de gør det, skal de synkronisere arbejdskopien med bufferen fra tid til anden ved hjælp af metoden reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Endelig kan en arbejdskopi gemmes på disken (hvorved den oprindelige kompileringsenhed erstattes) ved hjælp af metoden commitWorkingCopy.  

Nedenstående kodestykke opretter f.eks. en arbejdskopi i en kompileringsenhed via en tilpasset arbejdskopiejer. Kodestykket ændrer bufferen, afstemmer ændringerne, committer ændringerne til disken og sletter til slut arbejdskopien.

    // Hent oprindelig kompileringsenhed
    ICompilationUnit originalUnit = ...;
    
    // Hent arbejdskopiejer
    WorkingCopyOwner owner = ...;
    
    // Opret arbejdskopi
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Revidér buffer, og afstem
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Commit ændringer
    workingCopy.commitWorkingCopy(false, null);
    
    // Slet arbejdskopi
    workingCopy.discardWorkingCopy();

Flere klienter, der bruger en arbejdskopiejer, kan også være fælles om arbejdskopier. En arbejdskopi kan senere hentes ved hjælp af metoden findWorkingCopy. En fælles arbejdskopi indtastes således i den oprindelige kompileringsenhed og i en arbejdskopiejer.

Nedenfor vises, hvordan klient 1 opretter en fælles arbejdskopi, klient 2 henter denne arbejdskopi, klient 1 sletter arbejdskopien, og klient 2 prøver at hente bemærkningerne til den fælles arbejdskopi, der ikke findes mere:

    // Klient 1 & 2: Hent oprindelig kompileringsenhed
    ICompilationUnit originalUnit = ...;
    
    // Klient 1 & 2: Hent arbejdskopiejer
    WorkingCopyOwner owner = ...;
    
    // Klient 1: Opret fælles arbejdskopi
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Klient 2: Hent fælles arbejdskopi
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Dette er den samme arbejdskopi
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Klient 1: Slet fælles arbejdskopi
    workingCopyForClient1.discardWorkingCopy();
    
    // Klient 2: Forsøg at hente fælles arbejdskopi, og find ud af, at den er NULL
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Kodeændring via DOM/AST API

Du kan oprette en CompilationUnit på tre måder. Den første måde er at bruge ASTParser. Den anden måde er et bruge ICompilationUnit#reconcile(...). Den tredje måde er at starte helt forfra ved hjælp af fabriksmetoderne i AST (Abstract Syntax Tree).

Opret et AST fra eksisterende kildekode

Der skal oprettes en forekomst af ASTParser med ASTParser.newParser(int).

Kildekoden gives til ASTParser ved hjælp af en af følgende metoder: Derefter oprettes AST'et, når createAST(IProgressMonitor) kaldes.

Resultatet er et AST med korrekte kildepositioner for hver node. Løsningen af bindingerne skal bestilles, før træstrukturen oprettes med setResolveBindings(boolean). Løsning af bindinger er en kostbar affære, som kun bør udføres, når det er nødvendigt. Så snart træstrukturen er blevet ændret, går alle positioner og bindinger tabt.

Opret et AST ved at afstemme en arbejdskopi

Hvis en arbejdskopi ikke er konsistent (er blevet ændret), kan der oprettes et AST, ved at du kalder metoden reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Hvis du vil anmode om AST-oprettelse, skal du kalde metoden reconcile(...) med AST.JLS2 som den første parameter.

Bindingerne beregnes kun, hvis fejlanmoderen er aktiv, eller hvis der gennemtvinges fejlfinding. Løsning af bindinger er en kostbar affære, som kun bør udføres, når det er nødvendigt. Så snart træstrukturen er blevet ændret, går alle positioner og bindinger tabt.

Forfra

Det er muligt at oprette en CompilationUnit fra bunden ved hjælp af fabriksmetoderne i AST. Disse metoder begynder med new.... Nedenfor vises et eksempel, hvor klassen HelloWorld oprettes.

Det første kodestykke er det genererede output:

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

Følgende kodestykke indeholder den tilsvarende kode, som genererer outputtet.

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

Hent ekstra positioner

DOM/AST-node indeholder kun et par positioner (startpositionen og nodens længde). Det er ikke altid tilstrækkeligt. Hvis du vil hente mellemliggende positioner, skal du anvende IScanner API. For eksempel vil det være nyttigt at kende positionerne for operatoren instanceof for InstanceofExpression. Det kan opnås ved hjælp af følgende metode:
	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 bruges til at dele inputkilden i tokens. Hver token har en bestemt værdi, som defineres i grænsefladen ITerminalSymbols. Det er ret enkelt at gentage og hente den rigtige token. Det anbefales desuden, at du bruger scanneren, hvis du vil finde positionen for nøgleordet super i en SuperMethodInvocation.

Kildekodeændringer

Visse kildekodeændringer stilles ikke til rådighed via Java-elementets API. Du kan redigere kildekode, f.eks. ændre kildekoden for eksisterende elementer, på en mere generel måde ved at bruge kompileringsenhedens ubehandlede kildekode og derefter omskrive API'et for DOM/AST.

Der er to sæt API'er til omskrivning af DOM/AST: det beskrivende og det ændrende API.

Det beskrivende API ændrer ikke AST'et, men bruger API'et ASTRewrite til at generere beskrivelserne af ændringerne. AST-omskrivningsfunktionen opsamler beskrivelser af nodeændringer og konverterer beskrivelserne til redigeringstekster, der derefter kan anvendes på den oprindelige kilde.

   // oprettelse af et dokument
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // oprettelse af DOM/AST fra en ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // oprettelse af ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // beskrivelse af ændringen
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // beregning af de redigeringsteksterne
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // beregning af den nye kildekode
   edits.apply(document);
   String newSource = document.get();

   // opdatering af kompileringsenheden
   cu.getBuffer().setContents(newSource);

Via det ændrende API kan AST'et ændres direkte:

   // oprettelse af et dokument
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // oprettelse af DOM/AST fra en ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // start registrering af ændringerne
   astRoot.recordModifications();

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

   // beregning af de redigeringsteksterne
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // beregning af den nye kildekode
   edits.apply(document);
   String newSource = document.get();

   // opdatering af kompileringsenheden
   cu.getBuffer().setContents(newSource);

Reagér på ændringer i Java-elementer

Hvis din plugin har brug for at kende til ændringer af Java-elementer, efter de er foretaget, kan du registrere en Java IElementChangedListener med JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Du kan gøre det mere specifikt og angive den type aktivitet, du er interesseret i at bruge addElementChangedListener(IElementChangedListener, int).

Hvis du f.eks. kun er interesseret i at lytte efter aktiviteter under en afstemningsfunktion:

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

JavaCore understøtter to slags aktiviteter:

Lyttere til Java-elementændringer ligner begrebsmæssigt lyttere til ressourceændringer (der beskrives under Spor ressourceændringer). Følgende kodestykke implementerer en funktion til rapportering af Java-elementændringer, som udskriver elementdeltaerne til systemkonsollen.

   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 indeholder det element, der er blevet ændret, og markeringer, der beskriver, hvilken slags ændring der er foretaget. Oftest er deltatræstrukturens rod på Java-modelniveau. Klienter skal derefter navigere i deltaet ved hjælp af getAffectedChildren for at finde ud af, hvilke projekter der er blevet ændret.

Med følgende eksempelmetode gennemgås deltaen, og der bliver udskrevet elementer, der er tilføjet, fjernet og ændret:

    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");
                }
                /* Andre markeringer kan også blive komtrolleret */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Flere slags funktioner kan udløse en besked om Java-elementændring. Eksempler:

Ligner IResourceDelta, ved at Java-elementdeltaerne kan udføres i baggrunden ved hjælp af en IWorkspaceRunnable. De deltaer, der er resultat af flere Java-modelfunktioner, som udføres i en IWorkspaceRunnable, flettes og rapporteres på én gang.  

JavaCore indeholder metoden Udfør til udførsel af Java-elementændringer i baggrunden.

For eksempel vil følgende kodefragment udløse to funktioner til Java-elementændring:

    // Hent pakke
    IPackageFragment pkg = ...;
    
    // Opret to kompileringsenheder
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Mens følgende kodefragment vil udløse én aktivitet til Java-elementændring:

    // Hent pakke
    IPackageFragment pkg = ...;
    
    // Opret to kompileringsenheder
    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);