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 pela destruição da mesma quando não for mais necessária, utilizando o método destroy.

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(IProgressMonitor, IBufferFactory, IProblemRequestor). 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() ou o método reconcile(boolean,IProgressMonitor).

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

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

    // Obter a unidade de compilação original
    ICompilationUnit originalUnit = ...;
    
    // Obter o factory de buffer
    IBufferFactory factory = ...;
    
    // Criar a cópia de trabalho
    IWorkingCopy workingCopy = (IWorkingCopy)originalUnit.getWorkingCopy(null, factory, null);
    
    // Modificar o buffer e reconciliar
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile();
    
    // Consolidar as alterações
    workingCopy.commit(false, null);
    
    // Destruir a cópia de trabalho
    workingCopy.destroy();

As cópias de trabalho também podem ser compartilhadas por vários clientes. Uma cópia de trabalho compartilhada é criada utilizando-se o método getSharedWorkingCopy, que poderá ser recuperado posteriormente utilizando o método findSharedWorkingCopy. Uma cópia de trabalho compartilhada é, portanto, chaveada na unidade de compilação original e em um factory de buffer.

O seguinte exemplo mostra como o cliente 1 cria uma cópia de trabalho compartilhada, o cliente 2 a recupera, o cliente 1 a destrói 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 & 2: Obtém o factory de buffer
    IBufferFactory factory = ...;
    
    // Cliente 1: Cria a cópia de trabalho compartilhada
    IWorkingCopy workingCopyForClient1 = (IWorkingCopy)originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Cliente 2: Recupera a cópia de trabalho compartilhada
    IWorkingCopy workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
     
    // Esta é a mesma cópia de trabalho
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Cliente 1: Destrói a cópia de trabalho compartilhada
    workingCopyForClient1.destroy();
    
    // Cliente 2: Tenta recuperar a cópia de trabalho compartilhada e percebe que não existe
    workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
    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 uma unidade de compilação existente. A segunda é usar um arquivo de classe existente. A terceira é iniciar do zero utilizando os métodos de fábrica daAST (Abstract Syntax Tree).

Criando uma AST em uma Unidade de Compilação Existente

Isso pode ser obtido com os métodos parse na AST: Todos esses métodos definirão de adequadamente as posições de cada nó na árvore resultante. A resolução de ligações tem que ser solicitada antes da criação da árvore. 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 a partir de um arquivo de classe existente

Isto é obtido com o método de análise naAST: Este método definirá adequadamente as posições para cada nó na árvore resultante. A resolução de ligações tem que ser solicitada antes da criação da árvore. 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ção Genérica de Código Fonte

Algumas modificações de código fonte não são fornecidas por meio da API de elementos Java. Um modo mais geral de editar o código fonte (como alterar o código fonte para elementos existentes) é feito utilizando o código fonte bruto da unidade de compilação e o DOM Java.

Essas técnicas incluem o seguinte:

   // obter a origem de uma unidade de compilação
   Conteúdo da cadeia = myCompilationUnit.getBuffer().getContents();

   // Criar um JDOM editável
   myJDOM = new DOMFactory();
   myDOMCompilationUnit = myJDOM.createCompilationUnit(contents, "MyClass");

   // Navegar e editar a estrutura de unidade de compilação utilizando
   // o protocolo do nó JDOM.
   ...
// Depois que as modificações forem feitas em todos os nós,    // extraia de volta a origem do nó DOM da unidade de compilação.
   String newContents = myDOMCompilationUnit.getContents();

// Defina esse código de volta para dentro do elemento da unidade de compilaçãomyCompilationUnit.getBuffer().setContents(newContents);
// Salve o buffer no arquivo.   myCompilationUnit.save(null, false);

Essa técnica pode resultar em marcadores de problemas associados a números de linha incorretos, uma vez que os elementos Java não foram atualizados diretamente.

O gabarito de elementos Java não é mais apurado que os métodos e campos. A árvore de sintaxe abstrata utilizada pelo compilador não está disponível como API, portanto, as técnicas utilizadas pelo JDT para analisar a origem em estruturas programáticas não estão atualmente disponíveis como API.

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 antes da execução dos construtores:

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

Existem três tipos de eventos que são suportados por 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 paraIResourceDelta, 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);

Copyright IBM Corporation e outros 2000, 2003. Todos os Direitos Reservados.