Manipular código de 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 de Java es utilizar la API de elementos (element) de Java. Para trabajar con el código fuente sin procesar de un elemento de Java pueden utilizarse procedimientos más generales.

Modificar el código utilizando elementos de Java

Generar una unidad de compilación

La forma más fácil de generar desde programa 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 unidad de compilación ICompilationUnit nueva.

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

Modificar una unidad de compilación

Las modificaciones más sencillas del código fuente de Java pueden llevarse a cabo utilizando la API de elementos de Java.

Por ejemplo, desde una unidad de compilación puede consultarse un tipo. Una vez que tiene el IType, puede utilizar protocolos, como por ejemplo createField, createInitializer, createMethod o createType para añadir miembros de código fuente al tipos. En estos métodos se proporciona el código fuente e información sobre la ubicación de los miembros.

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

Copias de trabajo

Puede modificarse el código manipulando la unidad de compilación (y modificando con ello el IFile subyacente) o puede modificarse una copia en memoria de la unidad de compilación denominada copia de trabajo.

Una copia de trabajo se obtiene de una unidad de compilación utilizado el método getWorkingCopy. El usuario que crea la copia de trabajo es responsable de destruirla cuando ya no es necesaria mediante el método destroy.

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

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

Por ejemplo, el siguiente fragmento de código crea una copia de trabajo en una unidad de compilación utilizando una fábrica de almacenamiento intermedio de cliente. El fragmento modifica el almacenamiento intermedio, reconcilia los cambios, los compromete a disco y finalmente destruye la copia de trabajo.

    // Obtener la unidad de compilación original
    ICompilationUnit originalUnit = ...;
    
    // Obtener la fábrica de almacenamiento intermedio
    IBufferFactory factory = ...;
    
    // Crear una copia de trabajo
    IWorkingCopy workingCopy = originalUnit.getWorkingCopy(null, factory, null);
    
    // Modificar el almacenamiento intermedio y reconciliar
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile();
    
    // Comprometer los cambios
    workingCopy.commit(false, null);
    
    // Destruir la copia de trabajo
    workingCopy.destroy();

Las copias de trabajo también pueden compartirse entre varios clientes. Una copia de trabajo compartida se crea utilizando el método getSharedWorkingCopy y puede recuperarse más tarde mediante el método findSharedWorkingCopy. Por tanto, una copia de trabajo compartida contiene claves en la unidad de compilación original y en una fábrica de almacenamiento intermedio.

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 destruye y el cliente 2 descubre que ya no existe al intentar recuperarla:

    // Cliente 1 & 2: Obtener la unidad de compilación original
    ICompilationUnit originalUnit = ...;
    
    // Cliente 1 & 2: Obtener fábrica de almacenamiento intermedio
    IBufferFactory factory = ...;
    
    // Cliente 1: Crear la copia de trabajo compartida
    IWorkingCopy workingCopyForClient1 = originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Cliente 2: Recuperar la copia de trabajo compartida
    IWorkingCopy workingCopyForClient2 = originalUnit.findSharedWorkingCopy(factory);
     
    // Se trata de la misma copia de trabajo
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Cliente 1: Destruir la copia de trabajo compartida
    workingCopyForClient1.destroy();
    
    // Cliente 2: Intentar recuperar la copia de trabajo compartida y averiguar que es nula
    workingCopyForClient2 = originalUnit.findSharedWorkingCopy(factory);
    assert workingCopyForClient2 == null;

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

Existen dos formas de crear una CompilationUnit. La primera consiste en utilizar una unidad de compilación existente. La segunda consiste en empezar desde cero utilizando los métodos de fábrica de AST (Árbol de sintaxis abstracta).

Crear un AST a partir de una unidad de compilación existente

Se consigue con los métodos de análisis de AST: Todos estos métodos establecerán adecuadamente las posiciones de cada nodo en el árbol resultante. La resolución de enlaces debe solicitarse antes de la creación del árbol. La resolución de enlaces es una operación costosa, y sólo 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 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 es la salida generada:

	ejemplo de paquete;
   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 sólo 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 IScanner. Por ejemplo, tenemos una InstanceofExpression para 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;
	}
IScanner se utiliza para dividir el código fuente de entrada en símbolos. Cada símbolo tiene un valor especificado que se define en la interfaz ITerminalSymbols. Resulta bastante sencillo repetir y recuperar el símbolo correcto. También es aconsejable utilizar el escáner si se desea buscar la posición de la palabra clave super en una SuperMethodInvocation.

Modificación de código fuente genérico

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

Estos procedimientos son:

   // obtener el código fuente de una unidad de compilación
   String contents = myCompilationUnit.getBuffer().getContents();

   // Crear un JDOM editable
   myJDOM = new DOMFactory();
   myDOMCompilationUnit = myJDOM.createCompilationUnit(contents, "MyClass");

   // Recorrer y editar la estructura de la unidad de compilación utilizando
   // el protocolo de nodo de JDOM.
   ...
   // Una vez efectuadas las modificaciones en todos los nodos,
   // devolver el código fuente del nodo DOM de la unidad de compilación.
   String newContents = myDOMCompilationUnit.getContents();

   // Devolver este código al elemento de la unidad de compilación
   myCompilationUnit.getBuffer().setContents(newContents);

   // Guardar el almacenamiento intermedio en el archivo.
   myCompilationUnit.save();

Este procedimiento puede dar lugar a que algunos marcadores de problemas se asocien con números de línea incorrectos, porque los elementos de Java no se han actualizado directamente.

El modelo de elementos de Java no es mejor que los métodos y los campos. El árbol de sintaxis abstracta utilizado por el compilador no está disponible como API, de modo que los procedimientos utilizados por JDT para analizar el código fuente en estructuras programáticas no están disponibles actualmente en forma de API.

Responder a los cambios en los elementos de Java

Si un conector tiene que saber que ha ocurrido un cambio en un elemento de Java después de producirse, con JavaCore puede registrarse un escucha IElementChangedListener de Java.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Puede ser más concreto y especificar el tipo de eventos en los que está interesado al utilizar addElementChangedListener(IElementChangedListener, int).

Por ejemplo, si sólo está interesado en escuchar eventos antes de ejecutar los constructores:

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

Existen tres 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 apartado rastrear los cambios en los recursos). El fragmento de código siguiente implementa un informador de cambios en los elementos de 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);
         }
      }
   }

IJavaElementDelta incluye el elemento que se ha cambiado e indicadores que describen el tipo de cambio que se ha producido. La mayor parte del tiempo, la raíz del árbol delta está a nivel de modelo Java. A continuación, los clientes deben desplazarse a este delta utilizando el método getAffectedChildren para averiguar qué proyectos han cambiado.

El siguiente método de ejemplo recorre 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() + " se 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 indicadores */
      break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

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

Al igual que para IResourceDelta, los deltas de elemento Java pueden agruparse por lotes mediante un IWorkspaceRunnable. Los deltas resultantes de varias operaciones de modelo Java que se ejecutan dentro de un IWorkspaceRunnable se fusionan y notifican simultáneamente.

Por ejemplo, el siguiente 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 código desencadenará 1 evento de cambio de elemento Java:

    // Obtener paquete
    IPackageFragment pkg = ...;
    
    // Crear 2 unidades de compilación
    IWorkspace workspace = ResourcesPlugin.getWorkspace();
    workspace.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 y otros 2000, 2002. Reservados todos los derechos.