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に準拠したフォーマットで出力する必要がある。
お次は幾つか定数を追加した。
-
- AptConst.java
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
現コードではシンプルにXMLの要素をマップする際にJavaフィールドの型をString型に限定しているが、この部分に既存のライブラリィや独自の型コンバータを適用することでXML要素の値を別な型にマッピングすることが可能になるだろう。(実際にそのように使う予定だ)
この後の予定としては、上に書いたようにXML要素の文字列からJavaの型へのマッピング等、DAOへの応用を当て込みつつ、AndroidのActivityに対してDTOを自動的に生成する用途に使うかなと考えている。
※Visitorパターンは書いていてOOPぽいというか、覚えると面白いのでつい使いたくなるが、多用するとかえってコードの可読性が落ちる。程ほどに、本当に必要な場合のみ使うのが良いと思う。