かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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

以上