Java-koodin käsittely

Lisäosa voi luoda luokkia tai rajapintoja, lisätä metodeja olemassa oleviin tyyppeihin tai muuttaa tyyppien metodeja JDT-sovellusohjelmaliittymän avulla.

Helpoin tapa muuttaa Java-objekteja on käyttää Java-elementin sovellusohjelmaliittymää. Yleisempien tekniikoiden avulla voidaan käsitellä Java-elementin muotoilematonta lähdekoodia.

Koodin muutto Java-elementtien avulla

Käännösyksikön luonti

Helpoin tapa luoda käännösyksikkö ohjelmallisesti on käyttää IPackageFragment.createCompilationUnit-metodia. Määrität käännösyksikön nimen ja sisällön. Käännösyksikkö luodaan paketin sisään ja uusi ICompilationUnit palautuu.

Käännösyksikön voi luoda yleisesti luomalla tiedostoresurssin, jonka tunniste on ".java", sopivaan kansioon, joka vastaa paketin hakemistoa. Yleisen resurssisovellusohjelmaliittymän käyttö on takaportti Java-työkaluihin niin, että Java-mallia ei päivitetä, ennen kuin yleisten resurssien muutoksen kuuntelutoiminnoille on lähetetty ilmoitus ja JDT-kuuntelutoiminnot päivittävät Java-mallin uudella käännösyksiköllä.

Käännösyksikön muutto

Yksinkertaisimmat Java-lähteen muutokset voi tehdä Java-elementin sovellusohjelmaliittymällä.

Voit esimerkiksi kysellä tyyppiä käännösyksiköstä. Kun IType-rajapinta on olemassa, voit lisätä lähdekoodin jäseniä tyyppiin esimerkiksi createField-, createInitializer-, createMethod-, tai createType-käytännöllä. Lähdekoodi ja jäsenen sijainnin tiedot sisältyvät näihin metodeihin.

ISourceManipulation-rajapinta määrittää Java-elementtien yleiset lähteen muutokset. Tämä sisältää metodit tyypin jäsenen uudelleennimeämiseen, siirtoon, kopiointiin ja poistoon.

Työskentelykopiot

Koodia voi muuttaa käsittelemällä käännösyksikköä (jolloin pohjalla oleva IFile muuttuu) tai muuttamalla muistissa olevaa käännösyksikön kopiota, jota kutsutaan työskentelykopioksi.

Työskentelykopio saadaan käännösyksiköstä getWorkingCopy-metodilla. (Huomaa, että käännösyksikön ei tarvitse olla Java-mallissa, että työskentelykopion voi luoda.)  Kaikki työskentelykopion luojat ovat vastuussa sen poistosta, kun sitä ei enää tarvita, käyttämällä discardWorkingCopy-metodia.

Työskentelykopiot muuttavat muistissa olevaa puskuria. getWorkingCopy()-metodi luo oletuspuskurin, mutta työasemat voivat toteuttaa myös oman puskurinsa getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor)-metodilla. Työasemat voivat käsitellä tämän puskurin tekstiä suoraan. Jos näin tehdään, työskentelykopio on synkronoitava ajoittain reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor)-metodilla.

Lopuksi työskentelykopion voi tallentaa levyyn (jolloin se korvaa alkuperäisen käännösyksikön) commitWorkingCopy-metodilla.  

Esimerkiksi seuraava koodikatkelma luo käännösyksikön työskentelykopion mukautetulla työskentelykopion omistajalla. Katkelma muuttaa puskuria, täsmäyttää muutokset, vahvistaa muutokset levyyn ja lopuksi poistaa työskentelykopion.

    // Hae alkuperäinen käännösyksikkö
    ICompilationUnit originalUnit = ...;
    
    // Hae työskentelykopion omistaja
    WorkingCopyOwner owner = ...;
    
    // Luo työskentelykopio
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Muuta puskuri ja täsmäytä
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Vahvista muutokset
    workingCopy.commitWorkingCopy(false, null);
    
    // Tuhoa työskentelykopio
    workingCopy.discardWorkingCopy();

Työskentelykopiot voivat olla myös usean työaseman yhteiskäytössä työskentelykopion omistajan avulla. Työskentelykopion voi myöhemmin noutaa findWorkingCopy-metodilla. Yhteiskäytössä oleva työskentelykopio merkitään näin alkuperäiseen käännösyksikköön ja työskentelykopion omistajaan.

Seuraavassa näkyy, miten työasema 1 luo yhteisen työskentelykopion, työasema 2 noutaa tämän työskentelykopion, työasema 1 poistaa työskentelykopion ja yhteisen työskentelykopion noutoa yrittävä työasema 2 huomaa, ettei sitä ole enää olemassa:

    // Client 1 & 2: Hae alkuperäinen käännösyksikkö
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Hae työskentelykopion omistaja
    WorkingCopyOwner owner = ...;
    
    // Client 1: Luo yhteiskäytössä oleva työskentelykopio
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2: Nouda yhteiskäytössä oleva työskentelykopio
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Tämä on sama työskentelykopio
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Tuhoa yhteiskäytössä oleva työskentelykopio
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2: Yritys noutaa yhteiskäytössä oleva työskentelykopio ja sen huomaaminen, että se on null
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Koodin muutto DOM/AST-sovellusohjelmaliittymän avulla

Käännösyksikön voi luoda kolmella tavalla. Ensimmäinen tapa on käyttää ASTParser-jäsennystoimintoa. Toinen on ICompilationUnit#reconcile(...). Kolmas on aloittaa tyhjästä käyttämällä factory-metodejaAST:ssä (abstrakti syntaksirakenne).

AST-rakenteen luonti olemassa olevasta lähdekoodista

ASTParser-ilmentymä on luotava ASTParser.newParser(int)-metodilla.

Lähdekoodi annetaan ASTParser-jäsennystoimintoon jollakin seuraavista metodeista: Tämän jälkeen AST luodaan kutsumalla createAST(IProgressMonitor)-metodi.

Tuloksena on AST-rakenne, jossa on oikea lähteen paikka kullekin solmulle. Sidontojen tulkintaa on pyydettävä ennen rakenteen luomista metodilla setResolveBindings(boolean). Sidontojen tulkinta on vaikea toiminto, ja se tulee tehdä ainoastaan tarvittaessa. Kun rakennetta on muutettu, kaikki paikat ja sidonnat katoavat.

AST-rakenteen luonti täsmäyttämällä työskentelykopio

Jos työskentelykopio ei ole yhdenmukainen (sitä on muutettu), AST-rakenteen voi luoda kutsumalla metodi reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Pyydä AST-luontia kutsumalla reconcile(...) -metodi AST.JLS2 ensimmäisenä parametrina.

Sen sidonnat lasketaan vain, jos häiriöiden pyytäjä on käytössä tai jos häiriöiden tunnistus on pakotettu. Sidontojen tulkinta on vaikea toiminto, ja se tulee tehdä ainoastaan tarvittaessa. Kun rakennetta on muutettu, kaikki paikat ja sidonnat katoavat.

Tyhjästä

Käännösyksikön voi luoda tyhjästä käyttämällä AST-rakenteessa factory-metodeja. Näiden metodien nimen alussa on new.... Seuraava esimerkki luo HelloWorld-luokan.

Ensimmmäinen katkelma on luotu tuloste:

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

Seuraava katkelma on vastaava koodi, joka muodostaa tulosteen.

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

Ylimääräisten paikkojen nouto

DOM/AST-solmussa on vain yksi paikkapari (alkukohta ja solmun pituus). Tämä ei ole aina tarpeeksi. Välipaikkojen noutamista varten on käytettävä IScanner-sovellusohjelmaliittymää. Esimerkiksi meillä on InstanceofExpression, josta haluamme tietää instanceof-operaattorin paikat. Tämä saadaan selville kirjoittamalla seuraava metodi:
	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-sovellusohjelmaliittymää käytetään jakamaan syötelähde sanakkeiksi. Jokaisella sanakkeella on oma arvo, joka on määritetty ITerminalSymbols-rajapinnassa. On melko yksinkertaista iteroida ja noutaa oikea sanake. On suositeltavaa käyttää myös etsintäohjelmaa, jos haluat löytää super-avainsanan SuperMethodInvocation-lausekkeesta.

Lähdekoodin muutokset

Joitakin lähdekoodin muutoksia ei voi tehdä Java-elementin sovellusohjelmaliittymällä. Yleisempi tapa muokata lähdekoodia (esimerkiksi muuttaa olemassa olevien elementtien lähdekoodia) on käyttää käännösyksikön muotoilematonta lähdekoodia ja kirjoittaa DOM/AST-solmun sovellusohjelmaliittymä uudelleen.

DOM/AST-solmun uudelleenkirjoitusta varten on kaksi sovellusohjelmaliittymää: kuvaava uudelleenkirjoitus ja muuttava uudelleenkirjoitus.

Kuvaava sovellusohjelmaliittymä ei muuta AST-solmua, vaan muodostaa muutosten kuvauksen ASTRewrite-sovellusohjelmaliittymän avulla. AST-uudelleenkirjoitustoiminto kerää muutosten kuvaukset solmuihin ja kääntää kuvaukset tekstin muokkauksiksi, jotka voidaan tämän jälkeen toteuttaa alkuperäiseen lähteeseen.

   // Asiakirjan luonti
   ICompilationUnit cu = ... ; // sisältö on "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // DOM/AST-solmun luonti ICompilationUnit-käännösyksiköstä
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // ASTRewrite-luonti
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

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

   // tekstin muokkausten laskenta
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // uuden lähdekoodin laskenta
   edits.apply(document);
   String newSource = document.get();

   // käännösyksikön päivitys
   cu.getBuffer().setContents(newSource);

Muuttava sovellusohjelmaliittymä mahdollistaa AST-solmun suoran muutoksen:

   // Asiakirjan luonti
   ICompilationUnit cu = ... ; // sisältö on "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // DOM/AST-solmun luonti ICompilationUnit-käännösyksiköstä
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // aloita muutosten tallennus
   astRoot.recordModifications();

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

   // tekstin muokkausten laskenta
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // uuden lähdekoodin laskenta
   edits.apply(document);
   String newSource = document.get();

   // käännösyksikön päivitys
   cu.getBuffer().setContents(newSource);

Java-elementtien muutoksiin vastaaminen

Jos lisäosan tarvitsee tietää Java-elementteihin tarkistuksen jälkeen tehdyistä muutoksista, voit rekisteröidä Javan IElementChangedListener-rajapinnan JavaCore-lisäosan avulla.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Voit määrittää tarkemmin kiinnostuksen kohteena olevien tapahtumien tyypin käyttämällä addElementChangedListener(IElementChangedListener, int) -metodia.

Jos olet esimerkiksi kiinnostunut vain täsmäytyksen aikaisten tapahtumien kuuntelusta:

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

JavaCore tukee kahta tapahtumalajia:

Java-elementin muutoksen kuuntelutoiminnot ovat periaatteessa samankaltaisia kuin resurssin muutoksen kuuntelutoiminnot (kuvattu ohjeaiheessa resurssin muutosten seuranta). Seuraava katkelma toteuttaa Java-elementin muutoksen ilmoitustoiminnon, joka tulostaa elementin deltat järjestelmäkonsoliin.

   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 sisältää muutetun elementin ja määritteet, jotka kuvaavat tapahtuneen muutoksen lajia. Useimmiten deltarakenteen juuri on Java-mallin tasolla. Tällöin työasemien on selvitettävä muuttuneet projektit siirtymällä kyseiseen deltaan getAffectedChildren-metodin avulla.

Seuraava esimerkki kulkee deltan läpi ja tulostaa lisätyt, poistetut ja muutetut elementit:

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

Monenlaiset toiminnot voivat liipaista Java-elementin muutosilmoituksen. Seuraavassa on joitakin esimerkkejä:

Samon kuin IResourceDelta-deltoja, Java-elementtideltoja voi asettaa eräajoon IWorkspaceRunnable-rajapinnan avulla. Useiden IWorkspaceRunnable-rajapinnan sisällä ajettujen Java-mallitoimintojen tuloksena olevat deltat yhdistetään ja ilmoitetaan kerralla.  

JavaCore sisältää run-metodin, jolla Java-elementin muutokset voi asettaa eräajoon.

Esimerkiksi seuraava koodikatkelma liipaisee 2 Java-elementin muutostapahtumaa:

    // Hae paketti
    IPackageFragment pkg = ...;
    
    // Luo 2 käännösyksikköä
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Seuraava koodikatkelma puolestaan liipaisee 1 Java-elementin muutostapahtuman:

    // Hae paketti
    IPackageFragment pkg = ...;
    
    // Luo 2 käännösyksikköä
    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);