Manipulace s kódem v jazyce Java

Uživatelský modul plug-in může používat rozhraní API JDT k vytváření tříd a rozhraní, přidávání metod do stávajících typů a dále k pozměňování metod pro různé typy.

Nejjednodušším způsobem úpravy objektů Java je použití rozhraní API prvku Java. K práci se zdrojovým kódem prvku Java lze použít běžné přímé metody.

Úprava kódu s použitím prvků Java

Generování kompilační jednotky

Nejsnadnějším způsobem vygenerování kompilační jednotky v rámci programu je použití IPackageFragment.createCompilationUnit. Zadejte název a obsah kompilační jednotky. Dojde k vytvoření nové kompilační jednotky v balíčku a je vrácena nová ICompilationUnit.

Kompilační jednotku lze vytvořit běžným způsobem vytvořením souboru s příponou názvu ".java" v příslušné složce odpovídající adresáři balíčku. Použití obecného rozhraní API prostředků otevírá zadní vrátka do sady nástrojů platformy Java, a proto se model Java aktualizuje až po potvrzení běžných listenerů změn prostředků a dále po aktualizování modelu Java novou kompilační jednotkou prostřednictvím listenerů JDT.

Úprava kompilační jednotky

Nejjednodušší úpravy zdrojového textu v jazyce Java lze provádět prostřednictvím rozhraní API prvku Java.

Například můžete provádět dotazy na typ v kompilační jednotce. Jakmile získáte IType, můžete k přidání členů zdrojového kódu k typu použít protokoly createField, createInitializer, createMethod nebo createType. Tyto metody poskytují zdrojový kód a informaci o umístění člena.

Rozhraní ISourceManipulation definuje běžné manipulace se zdrojem prvků Java. Jde o metody přejmenování, přesouvání, kopírování a odstraňování člena typu.

Pracovní kopie

Kód lze upravovat manipulací s kompilační jednotkou (v tomto případě dochází k úpravě základního IFile) nebo lze upravit kopii kompilační jednotky umístěnou v paměti, tzv. pracovní kopii.

Pracovní kopii získáte z kompilační jednotky s použitím metody getWorkingCopy. (Povšimněte si, že pro vytvoření pracovní kopie nemusí kompilační jednotka existovat v modelu Java.)  Pokud někdo vytvoří takovouto pracovní kopii, nese odpovědnost za její likvidaci s použitím metody discardWorkingCopy.

Pracovní kopie upravují obsah vyrovnávací paměti v operační paměti. Metoda getWorkingCopy() sice vytváří výchozí vyrovnávací paměť, avšak klienti mohou disponovat vlastní implementací vyrovnávací paměti s použitím metody getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Klienti mohou s textem manipulovat přímo ve vyrovnávací paměti. Přitom však musí průběžně synchronizovat pracovní kopii s obsahem vyrovnávací paměti s použitím metody reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Pracovní kopii lze ukládat na disk (nahrazovat původní kompilační jednotku) s použitím metody commitWorkingCopy.  

Následující příklad úseku zdrojového kódu vytváří pracovní kopii pro kompilační jednotku s použitím uživatelského vlastníka pracovní kopie. Kód upraví obsah vyrovnávací paměti, zarovná změny, uloží tyto změny na disk a nakonec zlikviduje pracovní kopii.

    // Získat původní kompilační jednotku
    ICompilationUnit originalUnit = ...;
    
    // Získat vlastníka pracovní kopie
    WorkingCopyOwner owner = ...;
    
    // Vytvořit pracovní kopii
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Upravit obsah vyrovnávací paměti a srovnání
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Potvrdit změny
    workingCopy.commitWorkingCopy(false, null);
    
    // Zlikvidovat pracovní kopii
    workingCopy.discardWorkingCopy();

Klienti mohou s použitím vlastníka pracovní kopie sdílet pracovní kopie. Pracovní kopii lze později načíst metodou findWorkingCopy. Sdílená pracovní kopie je z tohoto důvodu přiřazena původní kompilační jednotce a vlastníkovi pracovní kopie.

Následující příklad předvádí postup, ve kterém klient 1 vytvoří sdílenou pracovní kopii, klient 2 tuto pracovní kopii načte, klient 1 pracovní kopii zlikviduje a klient 2 se pokusí o její načtení a zjistí, že již neexistuje:

    // Klient 1 & 2: Získat původní kompilační jednotku
    ICompilationUnit originalUnit = ...;
    
    // Klient 1 & 2: Získat vlastníka pracovní kopie
    WorkingCopyOwner owner = ...;
    
    // Klient 1: Vytvořit sdílenou pracovní kopii
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Klient 2: Načíst sdílenou pracovní kopii
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Je toto shodná pracovní kopie
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Klient 1: Zlikvidovat sdílenou pracovní kopii
    workingCopyForClient1.discardWorkingCopy();
    
    // Klient 2: Pokus o načtení sdílené kopie a zjištění, že jde o nulový objekt
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Úprava kódu s použitím rozhraní API DOM/AST

K dispozici jsou tři způsoby vytvoření Kompilační jednotky. První způsob: použití ASTParser. Druhý způsob: použití ICompilationUnit#reconcile(...). Třetí způsob: spuštění od začátku s použitím metod vytvoření objektů v AST (abstraktní syntaktický strom).

Vytváření AST ze stávajícího zdrojového kódu

Je zapotřebí vytvořit instanci ASTParser s použitím ASTParser.newParser(int).

Zdrojový kód je poskytnut syntaktickému analyzátoru ASTParser některou z těchto metod: Poté se vytvoří abstraktní syntaktický strom voláním createAST(IProgressMonitor).

Výsledkem je abstraktní syntaktický strom se správnými polohami zdrojů pro jednotlivé uzly. Rozlišení vazeb musí být vyžádáno před vytvořením stromu metodou setResolveBindings(boolean). Rozlišení vazeb je náročná operace a měla by být prováděna pouze v případě nezbytné potřeby. Po úpravě stromu dochází ke ztrátě všech poloh a vazeb.

Vytváření abstraktního syntaktického stromu sesouhlasením pracovní kopie

Není-li pracovní kopie konzistentní (je upravena), lze abstraktní syntaktický strom vytvořit voláním metody reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Vytvoření abstraktního syntaktického stromu vyžádáte voláním metody reconcile(...) s parametremAST.JLS2 na prvním místě.

Vazby se vypočítávají pouze v případě, že je aktivní klient problému nebo pokud je vynuceno rozpoznávání problémů. Rozlišení vazeb je náročná operace a měla by být prováděna pouze v případě nezbytné potřeby. Po úpravě stromu dochází ke ztrátě všech poloh a vazeb.

Od začátku

Objekt CompilationUnit lze vytvořit od začátku s použitím metod vytvoření vAST. Názvy těchto metod začínají řetězcem new.... Následuje příklad vytvoření třídy HelloWorld.

První úsek představuje vytvořený výstup:

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

Následující úsek představuje kód, který vytváří výstup.

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

Získání dalších pozic

Uzel DOM/AST obsahuje pouze pár pozic (počáteční a délka uzlu). To vždy nemusí postačovat. Chcete-li získat další pozice, použijte rozhraní API IScanner. Například mějme InstanceofExpression, pro který chceme získat pozice operátoru instanceof. K tomuto účelu lze použít následující metodu:
	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 se používá k rozčlenění vstupního zdroje na tokeny. Každý z tokenů má specifickou hodnotu definovanou v rozhraní ITerminalSymbols. Iterace a získání pravého tokenu je velmi jednoduché. Rovněž doporučujeme použít objekt scanner, chcete-li nalézt polohu klíčového slova super v SuperMethodInvocation.

Úpravy zdrojového kódu

Určité úpravy zdrojového kódu nejsou podporovány rozhraním API prvku Java. Obecnějším způsobem úpravy zdrojového kódu (např. úprava zdrojového kódu stávajících prvků) je přepis rozhraní API DOM/AST v čistém zdrojovém kódu kompilační jednotky.

Pro přepis DOM/AST jsou k dispozici dvě sady rozhraní API: popisný přepis a upravující přepis.

Popisné rozhraní API neupravuje AST, ale používá rozhraní API ASTRewrite k vytvoření popisů úprav. Přepisovač AST ukládá popisy úprav do uzlů a překládá tyto popisy do textových úprav, které lze uplatnit na původní zdrojový text.

   // vytvoření dokumentu
   ICompilationUnit cu = ... ; // obsahem je "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // vytvoření DOM/AST z ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // vytvoření ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // popis změny
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // výpočet úprav textu
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // výpočet nového zdrojového kódu
   edits.apply(document);
   String newSource = document.get();

   // aktualizace kompilační jednotky
   cu.getBuffer().setContents(newSource);

Úprava rozhraní API umožňuje upravovat přímo abstraktní syntaktický strom:

   // vytvoření dokumentu
   ICompilationUnit cu = ... ; // obsahem je "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // vytvoření DOM/AST z ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // spuštění záznamu úprav
   astRoot.recordModifications();

   // úprava abstraktního syntaktického stromu
   TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0)
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   typeDeclaration.setName(newName);

   // výpočet úprav textu
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // výpočet nového zdrojového kódu
   edits.apply(document);
   String newSource = document.get();

   // aktualizace kompilační jednotky
   cu.getBuffer().setContents(newSource);

Reakce na změny v prvcích Java

Pokud váš modul plug-in potřebuje znát provedené změny prvků Java, můžete registrovat listener Java IElementChangedListener s použitím JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Můžete být konkrétnější a zadat typ událostí, o které se zajímáte, s použitím addElementChangedListener(IElementChangedListener, int).

Zajímá-li vás například pouze naslouchání událostem během provádění srovnání (reconcile):

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

JavaCore podporuje dva typy událostí:

Listenery změny prvku Java mají strukturu obdobnou listenerům změny prostředku (viz popis ve sledování změn prostředků). Následující úsek zdrojového textu implementuje prvek vytvářející zprávy o změně prvku Java, který zobrazuje rozdílová data prvku v textové konzole.

   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 obsahuje prvek, který byl změněn, a příznaky popisující typ provedené změny. Po většinu času má strom rozdílových dat kořen na úrovni modelu Java. Klienti se v takovém případě musí přesouvat těmito rozdílovými daty s použitím getAffectedChildren za účelem vyhledání změněných projektů.

Následující metoda prochází rozdílovými daty a tiskne prvky, které byly přidány, odebrány nebo změněny:

    void traverseAndPrint(IJavaElementDelta delta) {
         switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " byl přidán");
      break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " byl odebrán");
      break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " byl změněn");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("Změna byla provedena v podřízeném prvku");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("Změna byla provedena v jeho obsahu");
                }
                /* Zkontrolovat lze rovněž další příznaky */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for(int i=0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Úkony určitých typů mohou spouštět potvrzování změn prvků Java. Uveďme několik příkladů:

Podobně jako IResourceDelta lze rozdílová data prvku Java dávkovat s použitím IWorkspaceRunnable. Rozdílová data vzniklá provedením určitého počtu úkonů modelu Java provedených v rámci IWorkspaceRunnable se spojí a jsou zobrazována společně.  

JavaCore poskytuje metodu run pro dávkové zpracování změn prvků Java.

Následující úsek zdrojového kódu spouští dvě události změny prvku Java:

    // Získat balíček
    IPackageFragment pkg = ...;
    
    // Vytvořit dvě kompilační jednotky
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Následující úsek zdrojového kódu spouští jednu událost změny prvku Java:

    // Získat balíček
    IPackageFragment pkg = ...;
    
    // Vytvořit dvě kompilační jednotky
    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);