Manipular código Java

El conector puede utilizar la API de JDT para crear clases o interfaces, añadir métodos a tipos existentes o modificar los métodos de los tipos.

La forma más fácil de modificar objetos Java es utilizar la API de elementos Java. Para trabajar con el código fuente en bruto de un elemento Java, pueden utilizarse técnicas más generales.

Modificación de código utilizando elementos Java

Generar una unidad de compilación

La forma más fácil de generar programáticamente una unidad de compilación es utilizando IPackageFragment.createCompilationUnit. Debe especificarse el nombre y el contenido de la unidad de compilación. Se creará la unidad de compilación en el paquete y se devolverá la interfaz ICompilationUnit nueva.

Una unidad de compilación puede crearse de forma genérica creando un recurso de archivo con la extensión ".java" en la carpeta apropiada que se corresponda con el directorio del paquete. La utilización de la API de recursos genérica es una puerta trasera de acceso a las herramientas Java, por lo que el modelo Java no se actualiza hasta que los escuchas genéricos de cambios de recurso reciben notificación y los escuchas de JDT actualizan el modelo Java con la unidad de compilación nueva.

Modificar una unidad de compilación

Las modificaciones más sencillas del fuente Java se pueden realizar con la API de elementos Java.

Por ejemplo, podrá consultar un tipo a partir de una unidad de compilación. Una vez que tiene la interfaz IType, puede utilizar protocolos como createField, createInitializer, createMethod o createType para añadir miembros de código fuente al tipo. En estos métodos se suministra el código fuente e información sobre la ubicación del miembro.

La interfaz ISourceManipulation define las manipulaciones del fuente más habituales para los elementos Java. Se incluyen métodos para cambiar el nombre de un miembro del tipo, moverlo, copiarlo o eliminarlo.

Copias de trabajo

El código se puede modificar manipulando la unidad de compilación (y con ello queda modificada la interfaz IFile subyacente) o bien se puede modificar una copia en memoria de la unidad de compilación, llamada copia de trabajo.

Se obtiene una copia de trabajo a partir de una unidad de compilación mediante el método getWorkingCopy. (Observe que, para crear una copia de trabajo, no hace falta que exista la unidad de compilación en el modelo Java). La persona que cree una copia de trabajo debe encargarse de descartarla cuando deje de ser necesaria mediante el método discardWorkingCopy.

Las copias de trabajo modifican un almacenamiento intermedio en memoria. El método getWorkingCopy() crea un almacenamiento intermedio predeterminado, pero los clientes pueden proporcionar su propia implementación de almacenamiento intermedio mediante el método getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Los clientes pueden manipular directamente el texto de este almacenamiento intermedio. Si lo hacen, deben sincronizar de vez en cuando la copia de trabajo con el almacenamiento intermedio mediante el método reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Por último, una copia de trabajo puede guardarse en disco (sustituyendo a la unidad de compilación original) mediante el método commitWorkingCopy.  

Por ejemplo, el siguiente fragmento de código crea una copia de trabajo en una unidad de compilación utilizando un propietario de copia de trabajo personalizada. El fragmento de código modifica el almacenamiento intermedio, reconcilia los cambios, los compromete en el disco y, por último, descarta la copia de trabajo.

    // Obtener la unidad de compilación original
    ICompilationUnit originalUnit = ...;
    
    // Obtener el propietario de la copia de trabajo
    WorkingCopyOwner owner = ...;
    
    // Crear una copia de trabajo
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Modificar el almacenamiento intermedio y reconciliar
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Comprometer los cambios
    workingCopy.commitWorkingCopy(false, null);
    
    // Destruir la copia de trabajo
    workingCopy.discardWorkingCopy();

Las copias de trabajo también pueden compartirse entre varios clientes mediante un propietario de copia de trabajo. Más adelante puede recuperarse una copia de trabajo mediante el método findWorkingCopy. Por lo tanto, una copia de trabajo compartida se articula en la unidad de compilación original y en un propietario de copia de trabajo.

El siguiente código muestra la forma en que el cliente 1 crea una copia de trabajo compartida, el cliente 2 recupera esta copia de trabajo, el cliente 1 la descarta y el cliente 2 descubre que ya no existe al intentar recuperarla:

    // Cliente 1 y 2: Obtener la unidad de compilación original
    ICompilationUnit originalUnit = ...;
    
    // Cliente 1 y 2: Obtener el propietario de la copia de trabajo
    WorkingCopyOwner owner = ...;
    
    // Cliente 1: Crear la copia de trabajo compartida
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Cliente 2: Recuperar la copia de trabajo compartida
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Es la misma copia de trabajo
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Cliente 1: Descartar la copia de trabajo compartida
    workingCopyForClient1.discardWorkingCopy();
    
    // Cliente 2: Intentar recuperar la copia de trabajo compartida y averiguar que es nula
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Modificación de código mediante la API de DOM/AST

Existen tres maneras de crear una clase CompilationUnit. La primera consiste en utilizar ASTParser. La segunda consiste en utilizar ICompilationUnit#reconcile(...). La tercera consiste en crearla a partir de cero mediante métodos de fábrica en AST (árbol de sintaxis abstracta).

Crear un AST a partir de código fuente existente

Debe crear una instancia de ASTParser con ASTParser.newParser(int).

El código fuente se proporciona a ASTParser con uno de los métodos siguientes: A continuación se crea el AST llamando a createAST(IProgressMonitor).

El resultado es un AST con las posiciones fuente correctas para cada nodo. La resolución de enlaces debe solicitarse antes de crear el árbol con setResolveBindings(boolean). La resolución de enlaces es una operación costosa, que solo debe realizarse cuando sea necesaria. En cuanto se ha modificado el árbol, se pierden todas las posiciones y enlaces.

Crear un ATA mediante la reconciliación de una copia de trabajo

Si una copia de trabajo no es coherente (se ha modificado), el ATA puede crearse llamando al método reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Para solicitar la creación de AST, llame al método reconcile(...) con AST.JLS2 como primer parámetro.

Sus enlaces solo se calculan el peticionario de problemas está activo o si se fuerza la detección de problemas. La resolución de enlaces es una operación costosa, que solo debe realizarse cuando sea necesaria. En cuanto se ha modificado el árbol, se pierden todas las posiciones y enlaces.

Desde cero

Es posible crear una clase CompilationUnit desde cero utilizando los métodos de fábrica de AST. Los nombres de estos métodos empiezan por new... A continuación se ofrece un ejemplo que crea una clase HelloWorld.

El primer fragmento de código es la salida generada:

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

El fragmento siguiente es el código correspondiente que genera la salida.

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

Recuperar posiciones adicionales

El nodo DOM/AST contiene solo un par de posiciones (la posición inicial y la longitud del nodo). Esto no siempre es suficiente. Para poder recuperar las posiciones intermedias, debe utilizarse la API de IScanner. Por ejemplo, tenemos una clase InstanceofExpression de la que deseamos saber las posiciones del operador instanceof. Para conseguirlo, podríamos escribir el siguiente método:
	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;
	}
La interfaz IScanner sirve para dividir el fuente de entrada en símbolos. Cada símbolo tiene un valor específico que se define en la interfaz ITerminalSymbols. Resulta bastante sencillo iterar y recuperar el símbolo correcto. También le recomendamos que utilice la exploración (scanner) si desea buscar la posición de la palabra clave super en una clase SuperMethodInvocation.

Modificaciones de código fuente

Algunas modificaciones del código fuente no son posibles mediante la API de elementos Java. Un procedimiento más general para editar el código fuente (como, por ejemplo, cambiar el código fuente de elementos existentes) consiste en utilizar el código fuente en bruto de la unidad de compilación y reescribir la API de DOM/AST.

Para realizar la reescritura de DOM/AST, hay dos conjuntos de API: reescritura descriptiva y reescritura de modificación.

La API descriptiva no modifica el AST, pero utiliza la API ASTRewrite para generar las descripciones de las modificaciones. El reescritor del AST recopila las descripciones de las modificaciones realizadas en los nodos y convierte dichas descripciones en ediciones de texto que pueden aplicarse a la fuente original.

   // Creación de un documento
   ICompilationUnit cu = ... ; // El contenido es "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // Creación de DOM/AST a partir de un ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // Creación de ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // Descripción del cambio
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // Cálculo de las ediciones de texto
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // Cálculo del código fuente nuevo
   edits.apply(document);
   String newSource = document.get();

   // Actualizar la unidad de compilación
   cu.getBuffer().setContents(newSource);

La API de modificación permite modificar el AST directamente:

   // Creación de un documento
   ICompilationUnit cu = ... ; // El contenido es "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // Creación de DOM/AST a partir de un ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // Registro inicial de las modificaciones
   astRoot.recordModifications();

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

   // Cálculo de las ediciones de texto
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // Cálculo del código fuente nuevo
   edits.apply(document);
   String newSource = document.get();

   // Actualizar la unidad de compilación
   cu.getBuffer().setContents(newSource);

Responder a los cambios realizados en los elementos Java

Si un conector tiene que saber que se han producido cambios en un elemento Java después de que hayan tenido lugar, podrá registrar una interfaz Java de escucha de elemnto cambiado, IElementChangedListener, en JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Puede ser más concreto y especificar el tipo de eventos que le interesan mediante el método addElementChangedListener(IElementChangedListener, int).

Por ejemplo, si solo le interesa estar a la escucha de eventos durante una operación de reconciliación:

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

Existen dos tipos de eventos soportados por JavaCore:

Los escuchas de cambios en los elementos Java se parecen conceptualmente a los escuchas de cambios en los recursos (que se describen en el tema rastrear los cambios en los recursos). El fragmento de código siguiente implementa un informador de cambios en los elementos Java que imprime los deltas del elemento en la consola del sistema.

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

La interfaz IJavaElementDelta incluye el elemento (element) que ha cambiado y los distintivos (flags) que describen el tipo de cambio producido. La mayor parte del tiempo, el árbol del delta está enraizado a nivel del modelo Java. Luego, los clientes deben navegar por este delta utilizando el método getAffectedChildren para averiguar qué proyectos han cambiado.

El siguiente método de ejemplo cruza (método traverse) un delta e imprime los elementos que se han añadido, eliminado y cambiado:

    void traverseAndPrint(IJavaElementDelta delta) {
         switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " se ha añadido");
      break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " se ha eliminado");
      break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " ha cambiado");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("El cambio se ha producido en sus hijos");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("El cambio se ha producido en su contenido");
                }
                /* También pueden comprobarse otros distintivos */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Existen varios tipos de operaciones que pueden desencadenar una notificación de cambio de elemento Java. A continuación se ofrecen algunos ejemplos:

De manera parecida a la interfaz IResourceDelta, los deltas de elemento Java se pueden procesar por lotes mediante una interfaz IWorkspaceRunnable. Los deltas resultantes de varias operaciones de modelo Java que se ejecutan dentro de una interfaz IWorkspaceRunnable se fusionan y notifican a la vez.  

La clase JavaCore proporciona un método run para procesar por lotes los cambios de los elementos Java.

Por ejemplo, el siguiente fragmento de código desencadenará 2 eventos de cambio de elemento Java:

    // Obtener paquete
    IPackageFragment pkg = ...;
    
    // Crear 2 unidades de compilación
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

Mientras que el siguiente fragmento de código desencadenará 1 evento de cambio de elemento Java:

    // Obtener paquete
    IPackageFragment pkg = ...;
    
    // Crear 2 unidades de compilación
    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);