Работа с кодом Java

Для создания классов или интерфейсов, добавления методов в существующие типы или изменения методов для типов в модуле можно применять API JDT.

Самый простой способ изменения объектов Java заключается в использовании API элементов Java. Для работы с первоначальным исходным кодом элемента Java можно применять более общие методы.

Изменение кода с помощью элементов Java

Генерация единицы компиляции

Метод IPackageFragment.createCompilationUnit предоставляет самый простой способ программной генерации единицы компиляции. Необходимо указать имя и содержимое единицы компиляции. Этот метод создает внутри пакета единицу компиляции и возвращает новый интерфейс ICompilationUnit.

В общем случае единица компиляции может создаваться путем создания ресурса файла с расширением ".java" в подходящей папке, которая соответствует каталогу пакета. Применение API общего ресурса позволяет незаметно входить в инструменты Java, поэтому модель Java не обновляется до тех пор, пока не будут оповещены приемники уведомлений об изменении общего ресурса, а приемники JDT не обновят модель Java с новой единицей компиляции.

Изменение единицы компиляции

Наиболее простые изменения исходного кода Java можно произвести с помощью API элементов Java.

Например, можно запросить тип из единицы компиляции. Если у вас есть IType, вы можете использовать протоколы createField, createInitializer, createMethod или createType для добавления в тип элементов исходного кода. В этих методах поставляется исходный код и информация о расположении элемента.

Интерфейс ISourceManipulation определяет общие действия над исходным кодом для элементов Java. Это методы для переименования, перемещения, копирования или удаления элемента типа.

Рабочие копии

Код можно изменить, изменяя или единицу компиляции (при этом изменяется и базовый интерфейс IFile), или ее копию "в памяти", называемую также рабочей копией.

Для получения рабочей копии из единицы компиляции предназначен метод getWorkingCopy. (Обратите внимание, что для создания рабочей копии не требуется, чтобы единица компиляции существовала в модели Java.)  Если рабочая копия больше не нужна, ее можно уничтожить с помощью метода discardWorkingCopy. За уничтожение рабочей копии отвечает то , кто ее создал.

Рабочие копии изменяют буфер "в памяти". Метод getWorkingCopy() создает буфер по умолчанию, однако клиенты могут предоставлять свою собственную реализацию буфера, используя метод getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor). Клиенты могут работать с текстом этого буфера напрямую. В этом случае они должны время от времени синхронизировать рабочую копию с буфером с помощью метода reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor).

Наконец, рабочую копию можно сохранить на диске (с заменой исходной единицы компиляции) с помощью метода commitWorkingCopy.  

Фрагмент кода в следующем примере создает рабочую копию для единицы компиляции, используя настраиваемого владельца рабочей копии. Этот фрагмент кода изменяет буфер, согласовывает изменения, сохраняет изменения на диске и, наконец, отбрасывает эту рабочую копию.

    // Получение исходной единицы компиляции
    ICompilationUnit originalUnit = ...;
    
    // Получение владельца рабочей копии
    WorkingCopyOwner owner = ...;
    
    // Создание рабочей копии
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Изменение буфера и согласование
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Сохранение изменений
    workingCopy.commitWorkingCopy(false, null);
    
    // Уничтожение рабочей копии
    workingCopy.discardWorkingCopy();

Применение владельца рабочей копии позволяет нескольким клиентам совместно использовать одну и ту же рабочую копию. Для последующего извлечения рабочей копии используется метод findWorkingCopy. Следовательно, доступ к общей рабочей копии осуществляется по исходной единице компиляции и по владельцу рабочей копии.

Приведенный ниже пример показывает, как клиент 1 создает общую рабочую копию, клиент 2 извлекает эту рабочую копию, клиент 1 уничтожает рабочую копию, а клиент 2, пытающийся извлечь рабочую копию, обнаруживает, что она больше не существует:

    // Клиент 1 & 2: Получение исходной единицы компиляции
    ICompilationUnit originalUnit = ...;
    
    // Клиент 1 & 2: Получение владельца рабочей копии
    WorkingCopyOwner owner = ...;
    
    // Клиент 1: Создание общей рабочей копии
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Клиент 2: Извлечение общей рабочей копии
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // Это одна и та же рабочая копия
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Клиент 1: Уничтожение общей рабочей копии
    workingCopyForClient1.discardWorkingCopy();
    
    // Клиент 2: Попытка извлечь общую рабочую копию; проверка, не пуста ли она
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

Изменение кода с помощью API DOM/AST

Существуют три способа создания CompilationUnit. Первый состоит в использовании класса ASTParser. При втором применяется метод ICompilationUnit#reconcile(...). Третий предполагает создание единицы компиляции с нуля с применением методов фабрики AST (Abstract Syntax Tree - Дерево абстрактного синтаксиса).

Создание AST из существующего исходного кода

Необходимо создать экземпляр класса ASTParser с методом ASTParser.newParser(int).

Исходный код передается в ASTParser с помощью одного из следующих методов: Затем для создания AST вызывается метод createAST(IProgressMonitor).

В результате будет создано дерево абстрактного синтаксиса с правильным расположением узлов в исходном коде. До создания дерева необходимо запросить разрешение связываний; для этого используется метод setResolveBindings(boolean). Разрешение связываний - это дорогостоящая операция, поэтому она должна выполняться только тогда, когда это необходимо. При изменении AST все позиции узлов и все связывания будут потеряны.

Создание AST путем согласования рабочей копии

Если рабочая копия не согласована (то есть она была изменена), то для создания AST можно вызывать метод reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor). Для запроса на создание AST вызовите метод reconcile(...), указав в качестве первого параметра AST.JLS2.

Ее связывания вычисляются только в том случае, если активен инициатор неполадок или включен принудительный поиск неполадок. Разрешение связываний - это дорогостоящая операция, поэтому она должна выполняться только тогда, когда это необходимо. При изменении AST все позиции узлов и все связывания будут потеряны.

Создание AST с нуля

Можно создать CompilationUnit с нуля, используя для AST методы фабрики. Имена этих методов начинаются с new.... Ниже приведен пример создания класса HelloWorld.

Первый фрагмент представляет собой генерированный вывод:

	package example;
	import java.util.*;
	public class HelloWorld {
		public static void main(String[] args) {
			System.out.println("Здравствуй," + " мир!");
		}
	}

Соответствующий фрагмент кода, генерирующий этот вывод, приведен ниже:

		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("Здравствуй,");
		infixExpression.setLeftOperand(literal);
		literal = ast.newStringLiteral();
		literal.setLiteralValue(" мир!");
		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);

Извлечение дополнительных позиций

Узел DOM/AST содержит только две позиции (начальную позицию и длину узла). Этого не всегда достаточно. Для извлечения промежуточных позиций необходимо использовать API IScanner. Пусть, например, существует узел InstanceofExpression, для которого требуется определить позиции операции instanceof. Для этого можно было бы написать следующий метод:
	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 позволяет разбить исходный фрагмент на лексемы. Каждая лексема имеет свое значение, которое определено в интерфейсе ITerminalSymbols. Довольно просто перебрать все лексемы и извлечь нужную. Если требуется определить позицию ключевого слова super в классе SuperMethodInvocation, также рекомендуется применять этот сканер.

Изменения исходного кода

Некоторые изменения исходного кода невозможно выполнить с помощью API элементов Java. Более общий способ изменения исходного кода (такой как изменение исходного кода существующих элементов) заключается в использовании первоначального исходного кода единицы компиляции и API перезаписи DOM/AST.

Существуют два набора API, позволяющие перезаписывать DOM/AST: описательный и изменяющий.

Описательный API не изменяет AST, а использует API ASTRewrite для генерации описаний изменений. Средство перезаписи AST собирает описания изменений в узлы и преобразует эти описания в текстовые изменения, которые затем могут быть применены к первоначальному исходному коду.

   // создание документа
   ICompilationUnit cu = ... ; // содержимое представляет собой "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // создание DOM/AST из ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // создание ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // описание изменения
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // вычисление изменений в тексте
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // вычисление нового исходного кода
   edits.apply(document);
   String newSource = document.get();

   // обновление единицы компиляции
   cu.getBuffer().setContents(newSource);

Изменяющий API позволяет вносить изменения непосредственно в AST:

   // создание документа
   ICompilationUnit cu = ... ; // содержимое представляет собой "public class X {\n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // создание DOM/AST из ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // начало регистрации изменений
   astRoot.recordModifications();

   // изменение AST
   TypeDeclaration typeDeclaration = (TypeDeclaration)astRoot.types().get(0)
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   typeDeclaration.setName(newName);

   // вычисление изменений в тексте
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // вычисление нового исходного кода
   edits.apply(document);
   String newSource = document.get();

   // обновление единицы компиляции
   cu.getBuffer().setContents(newSource);

Реакция на изменения в элементах Java

Если требуется, чтобы модуль "узнавал" об изменениях элементов Java постфактум, можно зарегистрировать интерфейс IElementChangedListener Java c помощью JavaCore.

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

Можно указать конкретный тип интересующих вас событий с помощью addElementChangedListener(IElementChangedListener, int).

Например, если модуль должен получать уведомления только о событиях, происходящих при операции согласования, укажите

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

JavaCore поддерживает события двух типов:

Приемники уведомлений об изменениях элементов Java подобны приемникам уведомлений об изменениях ресурсов (описанных в разделе отслеживание изменений ресурсов). Следующий фрагмент кода реализует средство создания отчетов об изменениях элементов Java, которое выводит дерево изменений на системную консоль.

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

Интерфейс IJavaElementDelta включает элемент, который был изменен, и флаги, описывающие тип произошедшего изменения. Большую часть времени корень дерева delta (дерева изменений) находится на уровне модели Java. Клиенты должны обойти ветви этого дерева с помощью метода getAffectedChildren, чтобы определить, какие проекты изменились.

Метод в следующем примере обходит дерево delta и печатает элементы, которые были добавлены, удалены или изменены:

    void traverseAndPrint(IJavaElementDelta delta) {
        switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " добавлен");
                break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " удален");
                break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " изменен");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("Его потомок был изменен");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("Его содержимое было изменено");
                }
                /* Можно проверить и другие флаги */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

Уведомление об изменении элементов Java может запускаться несколькими видами операций. Некоторые примеры приведены ниже:

Подобно IResourceDelta, интерфейс IWorkspaceRunnable позволяет группировать изменения элементов Java. Изменения, возникающие в результате нескольких операций модели Java, которые запускаются внутри IWorkspaceRunnable, объединяются и регистрируются одновременно.  

JavaCore предоставляет метод run для группирования изменений элементов Java.

Например, следующий фрагмент кода активирует два события изменения элементов Java:

    // Получение пакета
    IPackageFragment pkg = ...;
    
    // Создание 2 единиц компиляции
    ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
    ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

а этот фрагмент - одно событие:

    // Получение пакета
    IPackageFragment pkg = ...;
    
    // Создание 2 единиц компиляции
    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);