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