Manipulando o Código Java

O seu plug-in pode utilizar a API do JDT para criar classes ou interfaces, incluir métodos em tipos existentes ou alterar os métodos dos tipos.

O modo mais simples de alterar os objetos Java é utilizar a API de elementos Java. Técnicas mais gerais podem ser utilizadas para trabalhar com o código fonte bruto de um elemento Java.

Modificação de Código Utilizando Elementos Java

Gerando uma Unidade de Compilação

O modo mais fácil de gerar uma unidade de compilação de maneira programática, é utilizar IPackageFragment.createCompilationUnit.Você especifica o nome e o conteúdo da unidade de compilação. A unidade de compilação é criada dentro do pacote e a nova ICompilationUnit é retornada.

Uma unidade de compilação pode ser criada genericamente, criando-se um recurso de arquivo cuja extensão seja ".java" na pasta apropriada que corresponda ao diretório do pacote. A utilização da API de recursos genéricos fica na retaguarda das ferramentas Java, portanto, o gabarito Java não é atualizado até que os ouvintes de alteração de recurso genérico sejam notificados e os ouvintes JDT atualizem o gabarito Java com a nova unidade de compilação.

Modificando uma Unidade de Compilação

As modificações mais simples da origem Java podem ser feitas utilizando a API de elementos Java.

Por exemplo, você pode consultar um tipo a partir de uma unidade de compilação. Tendo o IType, você pode utilizar protocolos, tais como createField, createInitializer, createMethod ou createType para incluir membros de código fonte no tipo. O código fonte e as informações sobre a localização do membro são fornecidos nesses métodos.

A interface ISourceManipulation define manipulações de origem comum para elementos Java. Isso inclui métodos para renomear, mover, copiar ou excluir um membro do tipo.

Cópias de Trabalho

O código pode ser modificado manipulando-se a unidade de compilação (modificando, assim, o IFile de base) ou alguém pode modificar uma cópia da memória da unidade de compilação chamada de cópia de trabalho.

Uma cópia de trabalho é obtida em uma unidade de compilação utilizando-se o método getWorkingCopy.(Observe que a unidade de compilação não precisa existir no modelo Java para que uma cópia de trabalho seja criada.)  Aquele que criar essa cópia de trabalho será responsável pelo descarte da mesma quando não for mais necessária, utilizando o método discardWorkingCopy.

As cópias de trabalho modificam um buffer da memória. O método getWorkingCopy() cria um buffer padrão, mas os clientes podem fornecer sua própria implementação de buffer utilizando o método getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Os clientes podem manipular o texto deste buffer diretamente. Se fizerem isso, deverão sincronizar a cópia de trabalho com o buffer periodicamente utilizando o método reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Finalmente, uma cópia de trabalho pode ser salva no disco (substituindo a unidade de compilação original) utilizando o método commitWorkingCopy.   

Por exemplo, o trecho de código a seguir cria uma cópia de trabalho em uma unidade de compilação utilizando um proprietário de cópia de trabalho personalizado. O trecho modifica o buffer, reconcilia as alterações, consolida-as no disco e, finalmente, descarta a cópia de trabalho.

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

As cópias de trabalho também podem ser compartilhadas por vários clientes utilizando um proprietário de cópia de trabalho. Uma cópia de trabalho pode ser posteriormente recuperada utilizando o método findWorkingCopy. Uma cópia de trabalho compartilhada é, portanto, chaveada na unidade de compilação original e em um proprietário da cópia de trabalho.

O exemplo a seguir mostra como o cliente 1 cria uma cópia de trabalho compartilhada, o cliente 2 a recupera, o cliente 1 a descarta e o cliente 2, ao tentar recuperá-la, percebe que ela não existe mais:

    // Cliente 1 & 2: Obtém a unidade de compilação original
    ICompilationUnit originalUnit = ...;
    
    // Cliente 1 e 2: Obtêm o proprietário da cópia de trabalho
    WorkingCopyOwner owner = ...;
    
    // Cliente 1: Cria a cópia de trabalho compartilhada
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Cliente 2: Recupera a cópia de trabalho compartilhada
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Esta é a mesma cópia de trabalho
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Cliente 1: Descarta a cópia de trabalho compartilhada
    workingCopyForClient1.discardWorkingCopy();
    
    // Cliente 2: Tenta recuperar a cópia de trabalho compartilhada e percebe que não existe
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Modificação de Código Utilizando a API do DOM/AST

Há três maneiras de criar uma CompilationUnit. A primeira é utilizar ASTParser. A segunda é utilizar ICompilationUnit#reconcile(...). A terceira é iniciar do zero utilizando os métodos de fábrica daAST (Abstract Syntax Tree).

Criando uma AST a Partir de um Código Fonte Existente

Uma instância de ASTParser deve ser criada com ASTParser.newParser(int).

O código fonte é determinado para o ASTParser com um dos seguintes métodos: A seguir, a AST é criada chamando createAST(IProgressMonitor).

O resultado é uma AST com posições de origem corretas para cada nó. A resolução de ligações tem de ser pedida antes da criação da árvore com setResolveBindings(boolean). A resolução das ligações é uma operação dispendiosa e deve ser realizada apenas quando necessário. Assim que a árvore é modificada, todas as posições e ligações são perdidas.

Criando uma AST por Reconciliação de uma Cópia de Trabalho

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

Suas ligações serão calculadas apenas se o solicitante do problema estiver ativo ou se a detecção do problema for forçada. A resolução das ligações é uma operação dispendiosa e deve ser realizada apenas quando necessário. Assim que a árvore é modificada, todas as posições e ligações são perdidas.

Do Início

É possível criar uma CompilationUnit do início utilizando os métodos factory na AST. Os nomes desses métodos iniciam com new.... A seguir, um exemplo que cria uma classe HelloWorld.

O primeiro trecho é a saída gerada:

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

O trecho a seguir é 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);

Recuperando Posições Extras

O nó DOM/AST contém apenas um par de posições (a posição inicial e o comprimento do nó). Nem sempre isso é suficiente. Para recuperar posições intermediárias, a API IScanner deve ser utilizada. Por exemplo, temos uma InstanceofExpression da qual desejamos conhecer as posições do operador instanceof. Poderíamos gravar o seguinte método para que isso fosse obtido:
	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 origem da entrada em tokens. Cada token possui um valor específico que é definido na interface ITerminalSymbols. É muito simples repetir e recuperar o token correto. Também recomendamos que o scanner seja utilizado para procurar a posição da palavra-chave super em um SuperMethodInvocation.

Modificações do Código Fonte

Algumas modificações de código fonte não são fornecidas por meio da API de elementos Java. Uma maneira mais geral de editar o código fonte (como alterar o código fonte de elementos existentes) é realizada utilizando o código fonte bruto da unidade de compilação e a API de regravação do DOM/AST.

Para executar a regravação do DOM/AST, existem dois conjuntos de API: a regravação descritiva e a modificação de regravação.

A API descritiva não modifica a AST, mas utiliza a API ASTRewrite para gerar as descrições de modificações. O regravador da AST coleta descrições de modificações em nós e as converte em edições de texto que podem ser então aplicadas à fonte original.

   // criação de um Documento
   ICompilationUnit cu = ... ; // o conteúdo é "classe pública X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // criação de DOM/AST a partir 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();

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

A API de modificação permite modificar diretamente a AST:

   // criação de um Documento
   ICompilationUnit cu = ... ; // o conteúdo é "classe pública X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

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

   // iniciar registro das 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();

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

Respondendo a alterações em elementos Java

Se o seu plug-in precisar tomar conhecimento das alterações dos elementos Java após o fato, você poderá registrar um IElementChangedListener Java com JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Você pode especificar o tipo de evento em que está interessado utilizar addElementChangedListener(IElementChangedListener, int).

Por exemplo, se estiver interessado apenas em receber eventos durante uma operação de reconciliação:

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

Existem dois tipos de eventos suportados pelo JavaCore:

Os atendentes de alteração de elementos Java são semelhantes em conceito aos atendentes de alteração de recursos (descritos em Rastreando Alterações de Recursos). O trecho a seguir implementa um relator de alteração de elementos Java que imprime os deltas de elementos no console do sistema.

   public class MyJavaElementChangeReporter implements IElementChangedListener {
      public void elementChanged(ElementChangedEvent event) {
         IJavaElementDelta delta= event.getDelta();
         if (delta != null) {
            System.out.println("delta recebido: ");
            System.out.print(delta);
         }
      }
   }

O IJavaElementDelta inclui o elemento que foi alterado e os sinalizadores que descrevem o tipo de alteração ocorrido. Na maior parte do tempo, a árvore de deltas é colocada como raiz no nível de Gabarito Java. Em seguida, os clientes devem navegar por este delta utilizando getAffectedChildren para ver quais projetos foram alterados.

O exemplo de método a seguir passa por um delta e imprime os elementos que foram incluídos, removidos e alterados:

    void traverseAndPrint(IJavaElementDelta delta) {
        switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " foi adicionado");
                break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " foi removido");
                break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " foi alterado");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("A alteração estava em seus filhos");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("A alteração estava em seu conteúdo");
                }
                /* Outros sinalizadores também podem ser verificados */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Vários tipos de operações podem disparar uma notificação de alteração de elemento Java. Eis alguns exemplos:

Assim como para IResourceDelta, os deltas de elementos Java podem ser colocados em batch utilizando um IWorkspaceRunnable. Os deltas resultantes de várias operações de Gabarito Java executadas em um IWorkspaceRunnable são combinados e relatados imediatamente.   

JavaCore fornece um método de execução para colocar em batch alterações de elementos Java.

Por exemplo, o fragmento de código a seguir irá acionar 2 eventos de alteração de elementos Java:

    // Obter o pacote
    IPackageFragment pkg = ...;
    
    // Criar 2 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);

Ao passo que o fragmento de código a seguir irá acionar 1 evento de alteração de elementos Java:

    // Obter o pacote
    IPackageFragment pkg = ...;
    
    // Criar 2 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);