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 destruirla cuando deje de ser necesaria, mediante el método destroy

Las copias de trabajo modifican un almacenamiento intermedio en 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 directamente el texto de este almacenamiento intermedio. Si lo hacen, deben sincronizar la copia de trabajo con el almacenamiento intermedio de vez en cuando mediante el método reconcile() o el método 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 personalizado. El fragmento de código modifica el almacenamiento intermedio, reconcilia los cambios, los compromete en el 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 = (IWorkingCopy)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 lo tanto, una copia de trabajo compartida se articula 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 y 2: Obtener la unidad de compilación original
    ICompilationUnit originalUnit = ...;
    
    // Cliente 1 y 2: Obtener fábrica de almacenamiento intermedio
    IBufferFactory factory = ...;
    
    // Cliente 1: Crear la copia de trabajo compartida
    IWorkingCopy workingCopyForClient1 = (IWorkingCopy)originalUnit.getSharedWorkingCopy(null, factory, null);
    
    // Cliente 2: Recuperar la copia de trabajo compartida
    IWorkingCopy workingCopyForClient2 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
     
    // Es 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 = (IWorkingCopy)originalUnit.findSharedWorkingCopy(factory);
    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 una unidad de compilación existente. La segunda consiste en utilizar un archivo de clase existente. 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 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, que solo debe realizarse cuando sea necesaria. En cuanto se ha modificado el árbol, se pierden todas las posiciones y enlaces.

Crear un AST a partir de un archivo de clase existente

Esto se consigue con los métodos de análisis de AST: Este método establecerá 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, 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.

Modificación de código fuente genérico

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 el DOM Java.

Estos procedimientos son:

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

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

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

   // Establecer este código de nuevo en el elemento de la unidad de compilación
   myCompilationUnit.getBuffer().setContents(newContents);

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

Esta técnica puede dar lugar a que algunos marcadores de problemas queden asociados a números de línea incorrectos, porque los elementos Java no se han actualizado directamente.

El modelo de elementos Java no da mejores resultados que los métodos y los campos. El árbol de sintaxis abstracta (AST) utilizado por el compilador no está disponible como API, de modo que las técnicas utilizadas por JDT para analizar el fuente en estructuras programáticas no están disponibles actualmente en forma de API.

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 antes de que se ejecuten 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 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);

Copyright IBM Corporation y otras empresas 2000, 2003. Reservados todos los derechos.