Manipular o Código Java

O plug-in pode utilizar a API JDT para criar classes ou interfaces, adicionar métodos a tipos existentes ou alterar os métodos para tipos.

A forma mais fácil de alterar objectos Java é utilizar a API de elemento Java. Podem ser utilizadas mais técnicas gerais para trabalhar com código fonte em bruto para um elemento Java.

Modificação de código utilizando elementos Java

Gerar uma unidade de compilação

A forma mais fácil para gerar de forma programada uma unidade de compilação é utilizar IPackageFragment.createCompilationUnit. Indique o nome e conteúdo da unidade de compilação. A unidade de compilação é criada dentro do pacote e a nova ICompilationUnit é devolvida.

De forma geral, pode-se criar a unidade de compilação com um recursos de ficheiro cuja extensão seja ".java" na pasta adequada que corresponde ao directório do pacote. Com o recurso genérico, a API é uma porta das traseiras para utilizar as ferramentas Java, logo, o modelo Java não é actualizado até que o recurso genérico seja alterado e os ouvintes sejam notificados; os ouvintes JDT actualizam o modelo Java com a nova unidade de compilação.

Modificar uma unidade de compilação

As modificações mais simples de código Java podem ser feitas utilizando uma API de elemento Java.

Por exemplo, pode consultar um tipo de uma unidade de compilação. Assim que obtiver o IType, pode utilizar protocolos como createField, createInitializer, createMethod ou createType para adicionar membros de código fonte ao tipo. Estes métodos fornecem o código fonte e as informações sobre a localização do membro.

A interface ISourceManipulation define manipulações de fontes comuns em elementos Java. Isto inclui métodos para Mudar de nome, Copiar ou Eliminar um membro de um tipo.

Cópias de Trabalho

O Código pode ser modificado, manipulando a unidade de compilação (e o IFile subjacente é modificado) ou pode modificar-se uma cópia na memória da unidade de compilação denominada cópia de trabalho.

A cópia de trabalho é obtida de uma unidade de compilação utilizando o método getWorkingCopy. (Tenha em atenção que a unidade de compilação não tem de existir no modelo Java para que a cópia de trabalho seja criada.)  Quem criar essa cópia de trabalho fica responsável pela sua eliminação, quando deixar de ser necessária, utilizando o método discardWorkingCopy.

As cópias de trabalho modificam a memória tampão. O método getWorkingCopy() cria uma memória tampão predefinida, mas os clientes podem facultar a implementação da sua própria memória tampão com o método getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Os clientes podem manipular directamente o texto desta memória tampão. Se o fizerem, têm de sincronizar a cópia de trabalho com a memória tampão regularmente, utilizando o método reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Finalmente, é possível guardar uma cópia de trabalho no disco (substituindo a unidade de compilação inicial) com o método commitWorkingCopy.   

Por exemplo, o seguinte fragmento de código cria uma cópia de trabalho numa unidade de compilação com um proprietário personalizado de uma cópia de trabalho. O fragmento modifica a memória tampão, reconcilia as alterações, consolida as alterações no disco e finalmente rejeita a cópia de trabalho.

    // Obter unidade de compilação original
    ICompilationUnit originalUnit = ...;
    
    // Obter proprietário da cópia de trabalho
    WorkingCopyOwner owner = ...;
    
    // Criar cópia de trabalho
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Modificar memória tampão e reconciliar
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Consolidar alterações
    workingCopy.commitWorkingCopy(false, null);
    
    // Destrui cópia de trabalho
    workingCopy.discardWorkingCopy();

As cópias de trabalho podem ainda ser partilhadas por vários clientes utilizando o proprietária da cópia de trabalho. A cópia de trabalho pode ser obtida mais tarde com o método findWorkingCopy. Uma cópia de trabalho partilhada é, então, protegida por chave na unidade de compilação original e no proprietário da cópia de trabalho.

O seguinte fragmento mostra como o cliente 1 cria uma cópia de trabalho partilhada, o cliente 2 obtém essa cópia de trabalho, o cliente 1 rejeita a cópia de trabalho e o cliente 2 ao tentar obter a cópia de trabalho partilhada repara que esta já não existe:

    // Cliente 1 & 2: Obter unidade de compilação original
    ICompilationUnit originalUnit = ...;
    
    // Cliente 1 & 2: Obter proprietário da cópia de trabalho
    WorkingCopyOwner owner = ...;
    
    // Cliente 1: Criar cópia de trabalho partilhada
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Cliente 2: Obter cópia de trabalho partilhada
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Esta é a mesma cópia de trabalho
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Cliente 1: Rejeitar cópia de trabalho partilhada
    workingCopyForClient1.discardWorkingCopy();
    
    // Cliente 2: Tentar obter cópia de trabalho partilhada e descobri que é null
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Modificação de código utilizando a API DOM/AST

Existem três formas para criar uma CompilationUnit. A primeira é utilizar ASTParser. A segunda é utilizar ICompilationUnit#reconcile(...). A terceira e começar do princípio com os métodos factory numa AST (Abstract Syntax Tree - Árvore de Sintaxe Abstracta).

Criar uma AST a partir de um código fonte existente

A instância de ASTParser tem de ser criada com ASTParser.newParser(int).

O código fonte é dado ao ASTParser utilizando um dos métodos seguintes: A AST é então criada chamando createAST(IProgressMonitor).

O resultado é uma AST com as posições de fonte correctas para cada nó. A resolução de enlaces tem de ser pedida ante da criação da árvore com setResolveBindings(boolean). A resolução de enlaces é uma operação arriscada e só deve ser feita quando necessária. Quando a árvore é modificada, perdem-se todas as posições e enlaces.

Criar uma AST através da reconciliação de uma cópia de trabalho

Se a cópia de trabalho não for consistente (se tiver sido modificada), pode ser criada uma AST chamando o método reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Para pedir a criação de AST, chame o método reconcile(...) com AST.JLS2 como primeiro parâmetro.

Os respectivos enlaces são calculados apenas se o solicitador de problemas estiver activo ou se a detecção de problemas for obrigatória. A resolução de enlaces é uma operação arriscada e só deve ser feita quando necessária. Quando a árvore é modificada, perdem-se todas as posições e enlaces.

Do princípio

É possível criar uma CompilationUnit desde o princípio com os métodos factory numa AST. Os nomes destes métodos começam por new.... O seguinte é um exemplo que cria uma classe HelloWorld.

O primeiro fragmento é a saída gerada:

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

O fragmento seguinte é o código correspondente que gera a saída.

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

Obter mais posições

O nó DOM/AST contém apenas um par de posições (a posição de início e o comprimento do nó). Nem sempre isto é suficiente. Para obter posições intermédias, deve ser utilizada a API IScanner. Por exemplo, temos uma InstanceofExpression para a qual queremos saber as posições do operador instanceof. Para tal, podemos escrever o método seguinte:
	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;
	}
O IScanner é utilizado para dividir a fonte de entrada em sinais. Cada sinal tem um valor específico que está definido na interface ITerminalSymbols. É relativamente simples iterar e obter o sinal correcto. Recomenda-se ainda o uso do explorador se quiser encontrar a posição da palavra-chave super numa SuperMethodInvocation.

Modificações do código fonte

Algumas modificações de código fonte não são fornecidas pela API de elemento Java. A forma mais geral de editar o código fonte (como seja mudar o código fonte para os elementos existentes) é utilizar o código fonte em bruto da unidade de compilação e reescrever a API de DOM/AST.

Para reescrever DOM/AST, existem dois conjuntos de APIs: a reescrita descritiva e a reescrita modificadora.

A API descritiva não modifica a AST mas utiliza a API ASTRewrite para gerar as descrições de modificações. Esta recolhe as descrições de modificações a nós e traduz estas descrições em edições de texto que podem ser aplicadas ao código original.

   // criação de um Documento
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // criação de DOM/AST de uma ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // criação de ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // descrição da alteração
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // cálculo das edições de texto
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // cálculo do novo código fonte
   edits.apply(document);
   String newSource = document.get();

   // actualização da unidade de compilação
   cu.getBuffer().setContents(newSource);

A API modificador permite modificar directamente a AST:

   // criação de um Documento
   ICompilationUnit cu = ... ; // content is "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // criação de DOM/AST de uma ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // iniciar registo de modificações
   astRoot.recordModifications();

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

   // cálculo das edições de texto
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // cálculo do novo código fonte
   edits.apply(document);
   String newSource = document.get();

   // actualização da unidade de compilação
   cu.getBuffer().setContents(newSource);

Responder a alterações em elementos Java

Se o plug-in tiver de ser informado sobre as alterações a elementos Java depois de estas ocorrerem, pode registar um IElementChangedListener Java no JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Pode ser mais específico e indicar o tipo de eventos que interessam com o addElementChangedListener(IElementChangedListener, int).

Por exemplo, se só lhe interessar saber dos eventos durante uma operação de reconciliação:

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

Existem duas espécies de eventos suportados pelo JavaCore:

Os ouvintes de alteração a elementos Java têm um conceito semelhante aos ouvintes de alteração em recursos (descritos em rastrear alterações a recursos). O fragmento seguinte implementa um repórter de alteração a elemento Java que imprime os deltas do elemento na consola do sistema.

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

O IJavaElementDelta inclui o elemento que foi alterado e os sinalizadores que descrevem a espécie de alteração que ocorreu. A maioria das vezes, a árvore de deltas está enraizada no nível de Modelo Java. Os cliente têm, assim, de navegar neste delta com getAffectedChildren para encontrar os projectos que foram alterados.

O método exemplo seguinte atravessa um delta e imprime os elementos adicionados, removidos e alterados:

    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");
                }
                /* Podem verificar-se outros sinalizadores */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Várias espécies de operações podem activar a notificação de alteração a elemento Java. Apresentam-se alguns exemplos:

À semelhança de IResourceDelta, os deltas de elementos Java podem ser agrupados utilizando um IWorkspaceRunnable. Os deltas oriundos de várias operações de Modelo Java que são executados num IWorkspaceRunnable são fundidos e comunicados ao mesmo tempo.  

O JavaCore fornece um método run para agrupar alterações de elementos Java.

Por exemplo, o fragmento de código seguinte vai activar dois eventos de alteração a elementos Java:

  // Obter pacote
  IPackageFragment pkg = ...;
    
    // Criar duas unidades de compilação
    ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
    ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Enquanto que o fragmento de código seguinte vai activar um evento de alteração a elementos Java:

    // Obter pacote
    IPackageFragment pkg = ...;
    
    // Criar duas unidades de compilação
    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);