Manipulere Java-kode

Plugin-modulen kan bruke JDT-APIet til å opprette klasser eller grensesnitt, legge til metoder i eksisterende typer eller endre metodene for typer.

Den enkleste måten å endre Java-objekter på er å bruke Java-elementets API. Flere generelle teknikker kan brukes når du arbeider med rå kildekode for et Java-element.

Kodeendring med Java-elementer

Generere en kompileringsenhet

Den enkleste måten å generere en kompileringsenhet programmatisk på er å bruke IPackageFragment.createCompilationUnit. Du oppgir navnet på og innholdet i kompileringsenheten. Kompileringsenheten opprettes inne i pakken, og den nye ICompilationUnit returneres.

En kompileringsenhet kan opprettes generisk ved at det opprettes en filressurs med filtype ".java" i riktig mappe som svarer til pakkekatalogen. Bruk av generisk ressurs-API er en bakdør inn til bruk av Java-verktøy, så Java-modellen oppdateres ikke før den generiske ressursens endringslyttere underrettes og JDT-lytterne oppdaterer Java-modellen med den nye kompileringsenheten.

Endre en kompileringsenhet

De fleste enkle endringer av Java-kilde kan utføres ved hjelp av Java-elementets API.

Du kan for eksempel spørre om en type fra en kompileringsenhet. Når du har IType, kan du bruke protokoller som createField, createInitializer, createMethod, or createType til å legge til kildekodemedlemmer i typen. Kildekoden og informasjon om plasseringen til medlemmet følger med disse metodene.

Grensesnittet ISourceManipulation definerer vanlige kildemanipulasjoner for Java-elementer. Dette inkluderer metoder for navneendring, flytting, kopiering eller sletting av en types medlem.

Arbeidskopier

Kode kan endres ved at kompileringsenheten (og dermed den underliggende IFile) endres, eller man kan endre en kopi i minnet av kompileringsenheten, som kalles en arbeidskopi.

En arbeidskopi hentes fra en kompileringsenhet ved hjelp av metoden getWorkingCopy. (Merk at kompileringsenheten ikke behøver å finnes i Java-modellen for at en arbeidskopi skal opprettes.)  Den som oppretter en slik arbeidskopi, er ansvarlig for å fjerne den når den ikke lenger er nødvendig, ved hjelp av metoden discardWorkingCopy.

Arbeidskopier endrer en minnebuffer. Metoden getWorkingCopy() oppretter en standardbuffer, men klienter kan bruke sin egen bufferimplementering ved hjelp av metoden getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Klienter kan manipulere teksten i denne bufferen direkte. Hvis de gjør det, må de synkronisere arbeidskopien med bufferen en gang imellom ved hjelp av metoden reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

En arbeidskopi kan dessuten lagres til disk (og erstatte den opprinnelige kompileringsenheten) ved hjelp av metoden commitWorkingCopy.  

For eksempel oppretter kodesnutten nedenfor en arbeidskopi på en kompileringsenhet med en tilpasset arbeidskopieier. Snutten endrer bufferen, avstemmer endringene, setter i verk endringene på disk og fjerner arbeidskopien.

    // Hent opprinnelig kompileringsenhet
    ICompilationUnit originalUnit = ...;
    
    // Hent arbeidskopieier
    WorkingCopyOwner owner = ...;
    
    // Opprett arbeidskopi
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Endre buffer og avstem
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Iverksett endringer
    workingCopy.commitWorkingCopy(false, null);
    
    // Slett arbeidskopi
    workingCopy.discardWorkingCopy();

Arbeidskopier kan også deles av flere klienter ved hjelp av en arbeidskopieier. En arbeidskopi kan senere hentes inn ved hjelp av metoden findWorkingCopy. En delt arbeidskopi tastes inn på den opprinnelige kompileringsenheten og på arbeidskopieieren.

Følgende viser hvordan klient 1 oppretter en delt arbeidskopi, klient 2 henter denne arbeidskopien, klient 1 sletter arbeidskopien, og klient 2 prøver å hente den delte arbeidskopien og merker at den ikke lenger finnes:

    // Klient 1 & 2: Hent opprinnelig kompileringsenhet
    ICompilationUnit originalUnit = ...;
    
    // Klient 1 & 2: Hent arbeidskopieier
    WorkingCopyOwner owner = ...;
    
    // Klient 1: Opprett delt arbeidskopi
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Klient 2: Hent delt arbeidskopi
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Samme som arbeidskopi
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Klient 1: Slett delt arbeidskopi
    workingCopyForClient1.discardWorkingCopy();
    
    // Klient 2: Forsøk på å hente inn delt arbeidskopi, ser at den er null
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Kodeendring med DOM/AST API

Det er tre måter å opprette en CompilationUnit på. Den første er på bruke ASTParser. Den andre er å bruke ICompilationUnit#reconcile(...). Den tredje er å begynne fra grunnen av med factory-metoder på AST (abstrakt syntakstre).

Opprette en AST ut fra eksisterende kildekode

En forekomst av ASTParser må opprettes med ASTParser.newParser(int).

Kildekoden gis til ASTParser med en av følgende metoder: Så opprettes AST ved at createAST(IProgressMonitor) kalles opp.

Resultatet er en AST med riktige kildeposisjoner for hver node. Det må bes om oppløsning av bindinger før treet opprettes med setResolveBindings(boolean). Oppløsning av bindinger er en kostnadskrevende operasjon som bør utføres bare når det er nødvendig. Straks treet er endret, går alle posisjoner og bindinger tapt.

Opprette en AST ved å avstemme en arbeidskopi

Hvis en arbeidskopi ikke er konsistent (er blitt endret), kan en AST opprettes ved at metoden reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) kalles opp. For å be om AST-opprettelse kaller du opp metoden reconcile(...) med AST.JLS2 som første parameter.

Bindingene beregnes bare hvis problembestilleren er aktiv, eller hvis problemoppdaging tvinges på. Oppløsning av bindinger er en kostnadskrevende operasjon som bør utføres bare når det er nødvendig. Straks treet er endret, går alle posisjoner og bindinger tapt.

Fra grunnen av

Det er mulig å opprette en CompilationUnit fra grunnen av ved hjelp av factory-metoder på AST. Disse metodenavnene begynner med new.... Her er et eksempel som oppretter klassen HelloWorld.

Første snutt er genererte utdata:

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

Følgende snutt er tilsvarende kode som genererer utdataene:

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

Hente ekstraposisjoner

DOM/AST-noden inneholder bare et par posisjoner (startposisjon og nodelengden). Dette er ikke alltid nok. For å hente mellomliggende posisjoner bør du bruke APIet IScanner. For eksempel har vi en InstanceofExpression der vi ønsker å kjenne posisjonene for operatoren instanceof. For å gjøre dette kan vi skrive 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 brukes til å dele inndatakilden i symboler. Hvert symbol har en bestemt verdi som defineres i grensesnittet ITerminalSymbols. Det er ganske enkelt å gjenta og hente riktig symbol. Vi kan også anbefale at du brukeren skanneren hvis du vil finne posisjonen til nøkkelordet super i en SuperMethodInvocation.

Kildekodeendringer

Noen kildekodeendringer følger ikke med Java-element-APIet. En mer generell måte å redigere kildekoden på (for eksempel endre kildekoden for eksisterende elementer) får du ved å bruke kompileringsenhetens råkildekode og skrive om APIet for DOM/AST.

Når du skal utføre DOM/AST-omskriving, er det to sett med APIer: deskriptiv omskriving av endrende omskriving.

APIer for deskriptiv omskriving ender ikke AST, men bruker APIet ASTRewrite til å generere beskrivelsene av endringene. AST-omskriveren samler inn beskrivelser av endringer på noder og oversetter disse beskrivelsene til tekstredigeringer, som så kan brukes på originalkilden.

   // Opprett dokument
   ICompilationUnit cu = ... ; // Innhold er "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

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

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

   // Beskrivelse av endringen
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // Beregning av tekstredigeringer
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // Beregning av ny kildekode
   edits.apply(document);
   String newSource = document.get();

   // Oppdater kompileringsenhet
   cu.getBuffer().setContents(newSource);

Endrende API gjør det mulig å endre AST direkte:

   // Opprett dokument
   ICompilationUnit cu = ... ; // Innhold er "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

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

   // Start registrering av endringer
   astRoot.recordModifications();

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

   // Beregning av tekstredigeringer
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // Beregning av ny kildekode
   edits.apply(document);
   String newSource = document.get();

   // Oppdater kompileringsenhet
   cu.getBuffer().setContents(newSource);

Reagere på endringer i Java-elementer

Hvis plugin-modulen må vite om endringer i Java-elementer etter at de har skjedd, kan du for registrere en Java-IElementChangedListener med JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Du kan være mer spesifikk og oppgi typen hendelser du er interessert i, med addElementChangedListener(IElementChangedListener, int).

Hvis du for eksempel bare er interessert i å lytte etter hendelser under en avstemmingsoperasjon:

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

Det er to typer hendelser som støttes av JavaCore:

Java-elementendringslyttere er i prinsippet ganske like ressursendringslyttere (beskrevet i spore resultatendringer). Følgende snutt implementerer en endringsreporter for Java-element som skriver ut elementdeltaer på 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 inkluderer element, som er endret, og flagg som beskriver typen endring som har funnet sted. Det meste av tiden har deltatreet rot i Java-modellnivået. Klienter må så navigere i denne deltaen med getAffectedChildren for å finne ut hvilke prosjekter som er endret.

Følgende metodeeksempel traverserer en delta og skriver ut elementene som er lagt til, fjernet og endret:

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

Flere typer operasjoner kan utløse et endringsvarsel for Java-element. Her er noen eksempler:

I likhet med IResourceDelta kan Java-elementdeltaer kjøres satsvist ved hjelp av en IWorkspaceRunnable. Deltaene man får fra flere Java-modelloperasjoner som kjøres inne i en IWorkspaceRunnable, slås sammen og rapporteres under ett.  

JavaCore sørger for en run-metode for satsvis kjøring av Java-elementendringer.

For eksempel vil følgende kodefragment utløse to endringshendelser for Java-elementer:

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

Følgende kodefragment vil utløse en endringshandling for Java-elementer:

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