APTでテンプレートメソッドパターンを生成する その3

さて、最後にアノテーションプロセッサのコードを解説して終わろう。
処理の大部分、特にコードをWriterに出力する処理は以前に紹介したActionAnnotationProcessorと同様なため、抽象クラスAbstractBeanAnnotationProcessorクラスにプルアップした。

    • AbstractBeanAnnotationProcessor.java
package net.kazzz.annotation;

import static net.kazzz.annotation.AptConst.KEY_OVERWRITE_FLEEZE;

import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

@SupportedSourceVersion(SourceVersion.RELEASE_6)
public abstract class AbstractBeanAnnotationProcessor extends AbstractProcessor {
    protected final SimpleDateFormat iso8601 = 
        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    protected Filer filer;
    protected JavaFileObject fileObject;
    protected Writer writer;
    protected String outputPackage;

    protected void writeComment(String className) {
        this.format("/*\n");
        this.format(" * このファイルはアノテーションプロセッサ(" 
                + this.getClass().getName() + ")により自動生成されました。\n");
        this.format(" * file : %s.java;\n", className);
        this.format(" */\n");
    }

    protected void writeGeneratedAnnotation() {
        this.format("@Generated(\n");
        this.format("       value={\"%s\"}\n", this.getClass().getName());
        this.format("       , date=\"%s\")\n", iso8601.format(
                Calendar.getInstance().getTimeInMillis()));
    }

    protected void writeEndBrace() {
        this.format("}\n");
    }

    protected void format(String content, Object...args) {
        String formatted = String.format(content, args);
        try {
            this.writer.write(formatted);
        } catch (IOException e) {
            Messager messager = processingEnv.getMessager();
            messager.printMessage(Kind.ERROR, e.toString());
        }
    }

    protected final String capitalize(String name) {
        if (name.isEmpty()) {
            return name;
        }
        char[] chars = name.toCharArray();
        chars[0] = Character.toUpperCase(chars[0]);
        return new String(chars);
    }

    protected final String getPackageName(String fqcn) {
        int pos = fqcn.lastIndexOf('.');
        if (pos > 0) {
            return fqcn.substring(0, pos);
        }
        return null;
    }
    protected final String getShortClassName(String className) {
        int i = className.lastIndexOf('.');
        if (i > 0) {
            return className.substring(i + 1);
        }
        return className;
    }
}

特に解説するようなコードは無いが、writeGeneratedAnnotationメソッドは以前には無かったメソッドだ。
このメソッドはJava6で実装されたjavax.annotation.Generatedと同じであり、Androidで同アノテーションが用意されていないので、別に用意している。今の所は単なるマーカでしかないがプロセッサにより生成されたコードであることを判断する場合に使う予定で入れてある。date()属性にはISO 8601に準拠したフォーマットで出力する必要がある。

お次は幾つか定数を追加した。

public final class AptConst {
    public static final String KEY_AUTOREGISTER_OUTPUT_PACKAGE = "autoregister_output_package";
    public static final String KEY_AUTOREGISTER_GENERATE_CLASS = "autoregister_generateClass";
    public static final String KEY_AUTOBEAN_OUTPUT_PACKAGE = "autobean_output_package";
    public static final String KEY_XMLAUTOBEAN_OUTPUT_PACKAGE = "xmlautobean_output_package";
    public static final String VALUE_AUTOREGISTER_OUTPUT_PACKAGE = "net.kazzz.autoregister";
    public static final String VALUE_XMLAUTOBEAN_SUPER_CLASS = "net.kazzz.dto.xml.AbstractXmlDto";
}

解説は特にない。次は今回のアノテーションプロセッサの実装そのものだ。

    • XmlAutoBeanAnnotationProcessor.java
package net.kazzz.annotation;

import static net.kazzz.annotation.AptConst.KEY_XMLAUTOBEAN_OUTPUT_PACKAGE;
import static net.kazzz.annotation.AptConst.VALUE_XMLAUTOBEAN_SUPER_CLASS;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({"net.kazzz.annotation.XmlAutoBean"
    , "net.kazzz.annotation.Element"})
@SupportedOptions({KEY_XMLAUTOBEAN_OUTPUT_PACKAGE})
public class XmlAutoBeanAnnotationProcessor extends AbstractBeanAnnotationProcessor {

    protected int round;
    
    class ElementHolder {
        String name;
        String fieldName;
        String type;
    }
    
    /* (non-Javadoc)
     * @see javax.annotation.processing.AbstractProcessor#init(javax.annotation.processing.ProcessingEnvironment)
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //オプションパラメタの取得(指定がなければデフォルト値を使う)
        Map<String , String> options = this.processingEnv.getOptions();
        this.outputPackage = options.containsKey(KEY_XMLAUTOBEAN_OUTPUT_PACKAGE) 
            ? options.get(KEY_XMLAUTOBEAN_OUTPUT_PACKAGE)
            : null;
    
    }
    
    /* (non-Javadoc)
     * @see javax.annotation.processing.AbstractProcessor#process(java.util.Set, javax.annotation.processing.RoundEnvironment)
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 処理対象のルート要素を全て取得
        Set<? extends Element> roots = roundEnv.getRootElements();
        for (Element root : roots) {
            // ルート要素がクラスである場合
            if (root.getKind() == ElementKind.CLASS) {
                TypeElement typeElement = (TypeElement) root;
                for ( AnnotationMirror m : typeElement.getAnnotationMirrors()) {
                    //対象の型が@SupportedAnnotationTypesで指定した型に含まれていたら対象
                    String annotationName = m.getAnnotationType().asElement().toString();
                    if ( this.getSupportedAnnotationTypes().contains(annotationName)) {
                        this.outputBean(typeElement, m);
                    }
                }
            }
        } 
        return true;
    }

    @SuppressWarnings("unchecked")
    protected void outputBean(TypeElement typeElement, AnnotationMirror annotation) {
        Messager messager = processingEnv.getMessager();
        try {
            //getter
            boolean getter = true;
            // setter()
            boolean setter = true;
            // beandClass()
            String beanClass = typeElement.getSimpleName() + "Bean";
            // superClass()属性は今回は使わず固定のスーパクラスを使っている
            String superClass = VALUE_XMLAUTOBEAN_SUPER_CLASS;
            // elementts
            List<ElementHolder> elements = new ArrayList<ElementHolder>();
            
            //アノテーションの属性値を取得する
            Iterator<?> i = annotation.getElementValues().entrySet().iterator();
            while ( i.hasNext() ) {
                Map.Entry<ExecutableElement, AnnotationValue> m = 
                    (Map.Entry<ExecutableElement, AnnotationValue>)i.next();
                
                //getter
                if ( m.getKey().getSimpleName().contentEquals("getter") ) {
                    getter = (Boolean)m.getValue().getValue();
                    continue;
                }
                //setter
                if ( m.getKey().getSimpleName().contentEquals("setter") ) {
                    setter = (Boolean)m.getValue().getValue();
                    continue;
                }
                //beandClass
                if ( m.getKey().getSimpleName().contentEquals("beandClass") ) {
                    beanClass = (String)m.getValue().getValue();
                    // fqnの場合、そのクラスのパッケージを使用する
                    String pcackageName =  this.getPackageName(beanClass);
                    if ( pcackageName != null && !pcackageName.isEmpty() ) {
                        this.outputPackage = pcackageName;
                    }
                    beanClass = this.getShortClassName(beanClass);
                    continue;
                }
                
                //elements
                if ( m.getKey().getSimpleName().contentEquals("elements") ) {
                    //配列の要素は何故かListで取れる (配列ではない)
                    List<AnnotationValue> l = (List<AnnotationValue>)m.getValue().getValue();
                    for ( AnnotationValue av : l ) {
                        AnnotationMirror mirror = (AnnotationMirror)av.getValue();

                        String name = null;
                        String fieldName = null;
                        String type = "String";
                        Iterator<?> i2 = mirror.getElementValues().entrySet().iterator();
                        while ( i2.hasNext() ) {
                            Map.Entry<ExecutableElement, AnnotationValue> m2 = 
                                (Map.Entry<ExecutableElement, AnnotationValue>)i2.next();
                            //name()
                            if ( m2.getKey().getSimpleName().contentEquals("name") ) {
                                name = (String)m2.getValue().getValue();
                                fieldName = name;
                               
                            } else
                            //fieldName()
                            if ( m2.getKey().getSimpleName().contentEquals("fieldName") ) {
                                fieldName = (String)m2.getValue().getValue();
                               
                            } else
                            //type() 今回の実装ではString型へのマップしか行わない
                            /*
                            if ( m2.getKey().getSimpleName().contentEquals("type") ) {
                                type = (String)m2.getValue().getValue().toString();
                            }
                            */
                        }
                        if ( name != null && type != null ) {
                            ElementHolder eh = new ElementHolder();
                            eh.name = name;
                            eh.type = type;
                            eh.fieldName = fieldName;
                            elements.add(eh);
                        }
                    }
                    continue;
                }
            }
            
            //パッケージ名がまだ指定無しだった場合、元クラスのパッケージを使用
            if ( this.outputPackage == null ) {
                this.outputPackage = 
                    this.getPackageName(typeElement.toString());
            }
                
            String fqn = this.outputPackage + "." + beanClass;
            
            this.filer = processingEnv.getFiler();
            this.fileObject = this.filer.createSourceFile(fqn);
            this.writer = this.fileObject.openWriter();
    
            try {
                //コメント出力
                this.writeComment(fqn);
    
                //クラス定義部を出力
                this.writeClassDefinition(superClass, beanClass);
                
                //フィールド部を出力
                this.writeFieldDefinition(elements);
                
                //メソッド定義部を出力
                this.writeMethodDefinition(elements, getter, setter, superClass);
    
                this.writeEndBrace();
                
                this.writer.flush();
            } catch (Exception e ) {
                messager.printMessage(Kind.ERROR, e.getMessage());
                e.printStackTrace(new PrintWriter(this.writer));
            } finally {
                this.writer.close();
                messager.printMessage(Kind.NOTE,
                        "AutoBeanAnnotationProcessor Creating .. " + fileObject.toUri());
            }
        } catch (Exception e ) {
            messager.printMessage(Kind.ERROR, e.getMessage());
            e.printStackTrace(new PrintWriter(this.writer));
        }
    
    }

    protected void writeClassDefinition(String superClass, String className) {
        this.format("package %s;\n" , this.outputPackage);
        this.format("\n");
        this.format("import java.io.IOException;\n");
        this.format("\n");
        this.format("import org.xmlpull.v1.XmlPullParser;\n");
        this.format("import org.xmlpull.v1.XmlPullParserException;\n");
        this.format("\n");
        this.format("import net.kazzz.annotation.Generated;\n");
        
        this.format("\n");
        if ( !this.outputPackage.equals(this.getPackageName(superClass)) ) {
            this.format("import %s;\n", superClass);
            this.format("\n");
        }
        //Genareated アノテーション
        this.writeGeneratedAnnotation();
        
        //クラス定義
        this.format("public class %s extends %s {\n", className, this.getShortClassName(superClass));
    }

    private void writeFieldDefinition(List<ElementHolder> elements) {
        this.format("\n");
        for ( ElementHolder f : elements ) {
            this.format("    protected %s %s;\n", f.type, f.fieldName);
        }
        this.format("\n");
    }

    protected void writeMethodDefinition(List<ElementHolder> elements, boolean getter, boolean setter, String superclass) {
                
        Iterator<ElementHolder> i = elements.iterator();

        //parseメソッドのオーバライド
        this.format("    /* (non-Javadoc)\n");
        this.format("     * @see parse(" +  superclass + ")\n");
        this.format("     */\n");
        this.format("    @Override\n");
        this.format("    public void parse(XmlPullParser parser) throws XmlPullParserException, IOException {\n");
        this.format("\n");
        this.format("        int type;\n");
        this.format("        String tagName = \"\";\n");
        this.format("\n");
        this.format("        while((type = parser.next()) != XmlPullParser.END_DOCUMENT) {\n");
        this.format("            switch(type) {\n");
        this.format("            case XmlPullParser.START_DOCUMENT:\n");
        this.format("                break;\n");
        this.format("            case XmlPullParser.START_TAG:\n");
        this.format("                tagName = parser.getName();\n");
        this.format("                break;\n");
        this.format("            case XmlPullParser.TEXT:\n");
        while ( i.hasNext() ) {
            ElementHolder eh = i.next();
            
            this.format("                if( tagName.equalsIgnoreCase(\"%s\")) {\n", eh.name);
            this.format("                    this.set%s(parser.getText());\n", this.capitalize(eh.fieldName));
            if ( i.hasNext() ) {
                this.format("                } else \n");
            } else {
                this.format("                }\n");
                this.format("                break;\n");
            }
        }
        this.format("            case XmlPullParser.END_TAG:\n");
        this.format("                tagName = \"\";\n");
        this.format("                break;\n");
        this.format("            }\n");
        this.format("        }\n");
        this.format("    }\n");
        

        //アクセサの出力
        for ( ElementHolder f : elements ) {
            String typeName = f.type;
            String fieldName = f.fieldName;
            if ( getter ) {
                // ゲッターメソッドを出力 (booleanの場合はis〜)
                this.format("\n");
                if ( typeName.equalsIgnoreCase("boolean") || typeName.endsWith("Boolean") ) {
                    this.format("    public s% is%s() {\n", typeName, capitalize(fieldName));
                } else {
                    this.format("    public %s get%s() {\n", typeName, capitalize(fieldName));
                }
                this.format("        return this.%s;\n", fieldName);
                this.format("    }\n");
            }
            if ( setter ) {
                this.format("\n");
                this.format("    public void set%s(%s %s) {\n", capitalize(fieldName), typeName, fieldName );
                this.format("        this.%s = %s;\n", fieldName, fieldName);
                this.format("    }\n");
            }
        }
        
    }
}

Elementに記述されたアノテーションの属性値が任意の属性名一発で取得できず、Map.Entryの列挙を回す必要があるのと、コードのインデントが面倒な位で何も難しいことは無い。Elementの種別を判別してコードを書くのが煩わしい場合、Java6で追加された要素の種類に合わせた処理をVisitorパターンで書くためのジェネリクスTypeVisitorがあるが、これ位の処理であればVisitorの必要性はまだ感じていないので使っていない。※

現コードではシンプルにXMLの要素をマップする際にJavaフィールドの型をString型に限定しているが、この部分に既存のライブラリィや独自の型コンバータを適用することでXML要素の値を別な型にマッピングすることが可能になるだろう。(実際にそのように使う予定だ)

この後の予定としては、上に書いたようにXML要素の文字列からJavaの型へのマッピング等、DAOへの応用を当て込みつつ、AndroidのActivityに対してDTOを自動的に生成する用途に使うかなと考えている。

※Visitorパターンは書いていてOOPぽいというか、覚えると面白いのでつい使いたくなるが、多用するとかえってコードの可読性が落ちる。程ほどに、本当に必要な場合のみ使うのが良いと思う。