かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Visitorを使わないで選択されたクラスの情報にアクセスする

昔書いた記事で、TreePathScannerを使ってVisitorパターンを使ってクラスの情報をとってきてました。
http://blogs.wankuma.com/kazuki/archive/2007/12/20/113814.aspx

これ以外にも、やり方があることがわかったのでメモしておきます。2年越しの関連記事になります。
とりあえずTreeApiSampleという名前でNetBeansのモジュールプロジェクトを作成します。

ここに、クラスの情報をとっとくためのクラスをClazzという名前で作成します。

package okazuki.treeapi;

import java.util.ArrayList;
import java.util.List;

public class Clazz {
    // クラス名
    private String name;
    // メソッド名のリスト
    private List<String> methods = new ArrayList<String>();

    public List<String> getMethods() {
        return methods;
    }

    public void setMethods(List<String> methods) {
        this.methods = methods;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

上のクラスに、データを詰め込む仕事をするクラスを作成します。今回の目玉!まず、依存するライブラリをプロジェクトに追加します。追加するライブラリは、Java ソースとJavac API ラッパー, ファイルシステム API, データシステム APIの4つです。

そして、Clazzを作成するクラスなのでClazzMakerという名前のクラスを作成してFileObjectを受け取るmakeメソッドを作成します。とりあえず、どんがらだけ作成すると、以下のような感じです。このmakeメソッドを実装していきます。

package okazuki.treeapi;

import org.openide.filesystems.FileObject;

public class ClazzMaker {
    /**
     * 引数で渡されたFileObject内にある最初のクラス定義の情報を返す
     */
    public Clazz make(FileObject javaFileObject) {
        // TODO : 実装する
        return null;
    }
}

さくっと実行していきます。ポイントはCompilationUnitを使ってTreeを取得して、そいつに対して色々やってる点です。

package okazuki.treeapi;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.openide.filesystems.FileObject;

public class ClazzMaker {
    /**
     * 引数で渡されたFileObject内にある最初のクラス定義の情報を返す
     */
    public Clazz make(FileObject javaFileObject) {
        try {
            // FileObjectからJavaSourceを作成して
            JavaSource source = JavaSource.forFileObject(javaFileObject);
            if (source == null) return null;
            // タスクを実行
            MakeClazzTask task = new MakeClazzTask();
            source.runUserActionTask(task, true);
            // タスクの実行結果を返す
            return task.getCreatedClazz();
        } catch (IOException ex) {
            Logger.getLogger(ClazzMaker.class.getName()).log(Level.SEVERE, null, ex);
            // 駄目だったのでおしまい
            return null;
        }
    }

    /**
     * Clazzクラスの作成を実際にやるタスク
     */
    private static class MakeClazzTask implements Task<CompilationController> {
        // このタスクで作成されたクラスの情報
        private Clazz createdClazz;

        public void run(CompilationController parameter) throws Exception {
            // CompilationUnitを取得して
            CompilationUnitTree unit = parameter.getCompilationUnit();
            // 定義されている型を取得して
            List<? extends Tree> types = unit.getTypeDecls();
            // 何か定義されていて、クラスだったら
            if (types.isEmpty()) return;
            if (types.get(0).getKind() != Tree.Kind.CLASS) return;
            // 安心してClassTreeを取り出す
            ClassTree type = (ClassTree) types.get(0);

            // Clazzに名前を設定する
            Clazz clazz = new Clazz();

            // メソッドの情報を収集する
            clazz.setName(type.getSimpleName().toString());
            for (Tree member : type.getMembers()) {
                // メソッドじゃなかったら何もしない
                if (member.getKind() != Tree.Kind.METHOD) continue;
                // メソッドなので安心してキャスト
                MethodTree method = (MethodTree) member;
                clazz.getMethods().add(method.getName().toString());
            }
            
            // 最後までうまくいったのでフィールドに格納
            createdClazz = clazz;
        }

        public Clazz getCreatedClazz() {
            return createdClazz;
        }
    }
}

次に、こいつを使うアクションを作成します。TraceActionという名前で、TraceClassというラベルで作成しました。この中で、IOのAPIを使うのでライブラリに入出力 APIを追加しておきます。ClazzMakerを使うだけなので、さくっとやっちゃいます。

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package okazuki.treeapi;

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.windows.IOProvider;
import org.openide.windows.InputOutput;

public final class TraceAction implements ActionListener {

    private final DataObject context;

    public TraceAction(DataObject context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        FileObject obj = context.getLookup().lookup(FileObject.class);
        InputOutput io = IOProvider.getDefault().getIO("Trace", false);
        if (obj == null) {
            io.getOut().println("FileObject not found");
            return;
        }
        ClazzMaker maker = new ClazzMaker();
        Clazz clazz = maker.make(obj);
        if (clazz == null) {
            io.getOut().println("クラス情報の取得に失敗しました");
            return;
        }
        io.getOut().printf("%s : %s\n", clazz.getName(), clazz.getMethods());
    }
}

このモジュールを実行した先のNetBeansで簡単に以下のようなクラスを作成します。

package sampleapp;

public class Main {
    public static void main(String[] args) {}
    public void foo(){}
}

このクラスを選択した状態で、先ほど作ったメニューを選択すると、出力ウィンドウに以下のような結果が表示されます。

Main : [<init>, main, foo]

うん、ばっちりクラス名とメソッド名が取れてます。素敵。
クラス名を短い名前じゃなくてFQCNで取りたいときは、以下のようにしてClassTreeからTypeElementに変換できます。

CompilationUnitTree unit = parameter.getCompilationUnit();
Element elm = parameter.getTrees().getElement(parameter.getTrees().getPath(unit, type));
if (elm == null) return;
TypeElement typeElement = (TypeElement) elm;
// Clazzに名前を設定する
Clazz clazz = new Clazz();
clazz.setName(typeElement.getQualifiedName().toString());

以上