Java コードの操作

プラグインは JDT API を使用して、クラスまたはインターフェースを作成したり、既存の型にメソッドを追加したり、 あるいは型のメソッドを変更したりすることができます。

Java オブジェクトを最も簡単に変更するには、Java エレメント API を使用します。 生の Java エレメントのソース・コード自体を操作する場合は、さらに一般的な手法を使用できます。

Java エレメントを使用したコードの変更

コンパイル単位の生成

コンパイル単位をプログラマチックに生成する最も簡単な方法は、 IPackageFragment.createCompilationUnit を使用することです。 コンパイル単位の名前と内容を指定します。 コンパイル単位はパッケージ内に作成され、新しい ICompilationUnit が戻されます。

パッケージ・ディレクトリーに対応する該当フォルダーに「.java」という拡張子を持つファイル・リソースを作成することにより、 コンパイル単位を包括的に作成することができます。 汎用リソース API は、Java ツール作成の裏側で使用されるので、汎用リソース変更のリスナーが通知を受けるまで Java モデルの更新は行われず、 JDT リスナーは、新規コンパイル単位を使用して Java モデルを更新します。

コンパイル単位の変更

Java エレメント API を使用すると、Java ソースを最も簡単に変更することができます。

例えば、コンパイル単位から型を照会することができます。 IType を準備すると、 createFieldcreateInitializercreateMethod、 または createType のようなプロトコルを使用して、ソース・コード・メンバーを型に追加することができます。 ソース・コードおよびメンバーのロケーションに関する情報は、これらのメソッドで提供されます。

ISourceManipulation インターフェースは、Java エレメント操作の共通ソースを定義します。 これには、型のメンバーの名前変更、移動、コピー、または削除を行うメソッドが含まれます。

作業用コピー

コンパイル単位を操作することにより、コードを変更する (および IFile を変更する) か、作業用コピーと呼ばれるコンパイル単位のメモリー内コピーを変更することができます。

作業用コピーをコンパイル単位から取得するには、 getWorkingCopy メソッドを使用します。(Java モデルにコンパイル単位が存在しなくても、作業用コピーを作成できることに注意してください。)  作成した作業用コピーが不要になったときには、作成者が責任を持って discardWorkingCopy メソッドを使用してこのコピーを破棄します。

作業用コピーはメモリー内バッファーを変更します。 getWorkingCopy() メソッドはデフォルト・バッファーを作成しますが、 クライアントが、 getWorkingCopy(WorkingCopyOwner, IProblemRequestor, IProgressMonitor) メソッドを使用して独自のバッファー実装を準備することもできます。 クライアントはこのバッファーのテキストを直接操作することができます。 その場合、作業用コピーとバッファーをその時々で同期化しなければなりません。 そのためには、reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) メソッドを使用します。

最終的に作業用コピーを (元のコンパイル単位に置き換えて) ディスクに保管するには、 commitWorkingCopy メソッドを使用します。  

例えば、次のコードの断片では、カスタム作業用コピー所有者を使用してコンパイル単位に作業用コピーを作成します。 この断片は、バッファーを変更し、変更点を調整し、変更をディスクにコミットして、最後に作業用コピーを破棄します。

    // Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Get working copy owner
    WorkingCopyOwner owner = ...;
    
    // Create working copy
    ICompilationUnit workingCopy = originalUnit.getWorkingCopy(owner, null, null);
    
    // Modify buffer and reconcile
    IBuffer buffer = ((IOpenable)workingCopy).getBuffer();
    buffer.append("class X {}");
    workingCopy.reconcile(NO_AST, false, null, null);
    
    // Commit changes
    workingCopy.commitWorkingCopy(false, null);
    
    // Destroy working copy
    workingCopy.discardWorkingCopy();

作業用コピーは、作業用コピーの所有者を使用して、クライアント間でも共用可能です。 作業用コピーを後で検索するには、 findWorkingCopy メソッドを使用します。共用作業用コピーでは、元のコンパイル単位および作業用コピーの所有者にキーが付けられます。

次の例では、クライアント 1 が共用作業用コピーを作成し、クライアント 2 がこの作業用コピーを検索し、 クライアント 1 が作業用コピーを破棄し、クライアント 2 がその共用作業用コピーを検索しようとしますが、 それが存在していないことが示されます。

    // Client 1 & 2: Get original compilation unit
    ICompilationUnit originalUnit = ...;
    
    // Client 1 & 2: Get working copy owner
    WorkingCopyOwner owner = ...;
    
    // Client 1: Create shared working copy
    ICompilationUnit workingCopyForClient1 = originalUnit.getWorkingCopy(owner, null, null);
    
    // Client 2: Retrieve shared working copy
    ICompilationUnit workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
     
    // This is the same working copy
    assert workingCopyForClient1 == workingCopyForClient2;
    
    // Client 1: Discard shared working copy
    workingCopyForClient1.discardWorkingCopy();
    
    // Client 2: Attempt to retrieve shared working copy and find out it's null
    workingCopyForClient2 = originalUnit.findWorkingCopy(owner);
    assert workingCopyForClient2 == null;

DOM/AST API を使用するコード変更

CompilationUnit を作成するには、3 つの方法があります。 1 つめは、ASTParser を使用する方法です。 2 つめは、ICompilationUnit#reconcile(...) を使用する方法です。 3 つめは、AST (Abstract Syntax Tree: 抽象構文ツリー) 上のファクトリー・メソッドを使用して最初から作成する方法です。

既存のソース・コードからの AST の作成

ASTParser.newParser(int) を使用して、 ASTParser のインスタンスを作成する必要があります。

以下のいずれかのメソッドを使用して、ソース・コードを ASTParser に渡します。 そして、createAST(IProgressMonitor) を呼び出すことによって、AST が作成されます。

結果として、各ノードごとの正しいソース位置で AST が作成されます。 ツリーの作成前に、setResolveBindings(boolean) でバインディングの解決を要求しておく必要があります。 バインディングの解決はコストのかかる操作であるため、必要なときにのみ行ってください。 ツリーが変更されるとすぐに、すべての位置とバインディングが失われます。

作業用コピーの調整による AST の作成

作業用コピーに一貫性がない (変更された) 場合、メソッド reconcile(int, boolean, WorkingCopyOwner, IProgressMonitor) を呼び出して AST を作成することができます。 AST の作成を要求するには、AST.JLS2 を最初のパラメーターとして指定して、reconcile(...) メソッドを呼び出します。

問題リクエスターがアクティブな場合、または問題検出が強制された場合にのみ、バインディングが計算されます。 バインディングの解決はコストのかかる操作であるため、必要なときにのみ行ってください。 ツリーが変更されるとすぐに、すべての位置とバインディングが失われます。

最初から

AST 上のファクトリー・メソッドを使用して、最初から CompilationUnit を作成することが可能です。 これらのメソッド名は new... で始まります。 次の例では、HelloWorld クラスを作成します。

最初の断片は生成された次の出力です。

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

次の断片は、出力を生成するための対応コードです。

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

追加位置の検索

DOM/AST ノードには、位置のペア (開始位置とノードの長さ) のみが含まれます。 これでは十分とは言えません。 中間位置を検索するには、 IScanner API を使用する必要があります。 例えば、instanceof 演算子の位置を知るには、 InstanceofExpression が用意されています。 これを行うために、次のメソッドを作成することができます。
	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 インターフェースに定義されています。 正しいトークンを繰り返し、検索するのは比較的簡単です。 SuperMethodInvocation 内の super キーワードの位置を検出したい場合は、スキャナーを使用することもお勧めします。

ソース・コードの変更

Java エレメント API からでは行えないソース・コード変更もあります。 ソース・コードをより一般的な方法 (例えば、既存のエレメントのソース・コードを変更するなど) で編集するには、コンパイル単位の未加工のソース・コードと DOM/AST の再書き込み API を使用します。

DOM/AST の再書き込みを実行するための API は、2 つのセットがあります。 記述再書き込みと変更再書き込みです。

記述 API は AST を変更しませんが、ASTRewrite API を使用して変更の記述を生成します。 AST の再書き込み機能は、ノードに対する変更の記述を集めて、これらの記述をテキスト編集に変換します。 さらに、それらのテキスト編集をオリジナル・ソースに適用できます。

   // creation of a Document
   ICompilationUnit cu = ... ; // content is "public class X {¥n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // creation of DOM/AST from a ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // creation of ASTRewrite
   ASTRewrite rewrite = new ASTRewrite(astRoot.getAST());

   // description of the change
   SimpleName oldName = ((TypeDeclaration)astRoot.types().get(0)).getName();
   SimpleName newName = astRoot.getAST().newSimpleName("Y");
   rewrite.replace(oldName, newName, null);

   // computation of the text edits
   TextEdit edits = rewrite.rewriteAST(document, cu.getJavaProject().getOptions(true));

   // computation of the new source code
   edits.apply(document);
   String newSource = document.get();

   // update of the compilation unit
   cu.getBuffer().setContents(newSource);

変更 API を使用すると、AST を直接変更することができます。

   // creation of a Document
   ICompilationUnit cu = ... ; // content is "public class X {¥n}"
   String source = cu.getBuffer().getContents();
   Document document= new Document(source);

   // creation of DOM/AST from a ICompilationUnit
   ASTParser parser = ASTParser.newParser(AST.JLS2);
   parser.setSource(cu);
   CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);

   // start record of the modifications
   astRoot.recordModifications();

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

   // computation of the text edits
   TextEdit edits = astRoot.rewrite(document, cu.getJavaProject().getOptions(true));

   // computation of the new source code
   edits.apply(document);
   String newSource = document.get();

   // update of the compilation unit
   cu.getBuffer().setContents(newSource);

Java エレメントの変更に対する応答

Java エレメントに変更が加えられた後に、プラグインにそれを認識させる必要がある場合には、 Java の「IElementChangedListener」を 「JavaCore」に登録することができます。

   JavaCore.addElementChangedListener(new MyJavaElementChangeReporter());

addElementChangedListener(IElementChangedListener, int) の使用に関心があるイベントの型を、さらに特定して指定することができます。

例えば、調整操作中に、イベントを listen することのみに関心がある場合は、以下のようにします。

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

JavaCore でサポートされるイベントは 2 種類あります。

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 には、変更された element が組み込まれていて、 flags には発生した変更の種類が説明されています。 ほとんどの場合、デルタ・ツリーは、「Java モデル」レベルがルートとなります。 その後、クライアントはこのデルタをナビゲートするために getAffectedChildren を使用してどのプロジェクトが変更されたかを検出します。

次のメソッド例では、デルタをトラバースして、追加、変更、および除去されたエレメントを印刷します。

    void traverseAndPrint(IJavaElementDelta delta) {
        switch (delta.getKind()) {
            case IJavaElementDelta.ADDED:
                System.out.println(delta.getElement() + " was added");
                break;
            case IJavaElementDelta.REMOVED:
                System.out.println(delta.getElement() + " was removed");
                break;
            case IJavaElementDelta.CHANGED:
                System.out.println(delta.getElement() + " was changed");
                if ((delta.getFlags() & IJavaElementDelta.F_CHILDREN) != 0) {
                    System.out.println("The change was in its children");
                }
                if ((delta.getFlags() & IJavaElementDelta.F_CONTENT) != 0) {
                    System.out.println("The change was in its content");
                }
                /* Others flags can also be checked */
                break;
        }
        IJavaElementDelta[] children = delta.getAffectedChildren();
        for (int i = 0; i < children.length; i++) {
            traverseAndPrint(children[i]);
        }
    }

各種操作を使用して、Java エレメント変更の通知を起動することができます。 以下はその例です。

IResourceDelta と同様に、Java エレメントのデルタは IWorkspaceRunnable を使用してバッチ化されます。 IWorkspaceRunnable の内部で実行される各種「Java モデル」操作から得たデルタは、直ちにマージされ、報告されます。  

JavaCore は、Java エレメント変更をバッチ化するための実行メソッドを提供します。

例えば、次のコードの断片は 2 つの Java エレメントの変更イベントを起動します。

    // Get package
    IPackageFragment pkg = ...;
    
    // Create 2 compilation units
 	            ICompilationUnit unitA = pkg.createCompilationUnit("A.java", "public class A {}", false, null);
 	            ICompilationUnit unitB = pkg.createCompilationUnit("B.java", "public class B {}", false, null);

それに対して、次に示すコードの断片は 1 つの Java エレメントの変更イベントを起動します。

    // Get package
    IPackageFragment pkg = ...;
    
    // Create 2 compilation units
    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);