s2jdbc-genでコメントを管理したい

前置き

管理したいですよね。
というわけで調べてみたらこういうエントリがありました。
http://d.hatena.ne.jp/akiraneko/20081009/1223562878
DB → コメントSQLなAntタスクです。
しかし…そうじゃなくて、Entity → コメントSQLがやりたい。
やりかたもこのエントリをぱくれば参考にすればできそうです。

主な内容

当然ですが、classファイルにはコメントは含まれていないので、ソースファイルからコメントを取得します。
コメントのために新しいアノテーションが必要だなんて嫌ですし。
というわけで、Javadocの力を借ります。

準備

Javadocを使うのでtools.jarが必要です。
s2jdbc-gen-build.xmlで定義したclasspathに放り込みます。/WEB-INF/libあたり。

s2jdbc-gen-build.xml

<taskdef name="gen-comment" classname="org.seasar.extension.jdbc.gen.task.GenerateCommentTask" classpathref="classpath"/>
<target name="gen-comment">
  <gen-comment
    classpathdir="${classpathdir}"
    rootpackagename="${rootpackagename}"
    entitypackagename="${entitypackagename}"
    env="${env}"
    jdbcmanagername="${jdbcmanagername}"
    javafiledestdir="${javafiledestdir}"
    javafileencoding="${javafileencoding}"
    classpathref="classpath"
  />
</target>

基本的にはgen-ddlをぱくります。
ソースからコメントを読み込むので、javafiledestdirとjavafileencodingを追加します。

GenerateCommentTask

GenerateDdlTaskをぱくります。

protected GenerateCommentCommand command = new GenerateCommentCommand();

public void setJavaFiledestDir(File JavaFileDestDir) {
	command.setJavaFileDestDir(JavaFileDestDir);
}

public void setJavaFileEncoding(String javaFileEncoding) {
	command.setJavaFileEncoding(javaFileEncoding);
}

s2jdbc-gen-build.xmlに追加した属性をGenerateCommentCommandにセットするために、セッター?を追加します。
CommandはGenerateCommentCommandに差し替えておきます。

GenerateCommentCommand

GenerateDdlCommandをぱくります。
ちょっと長いので分割して載せます。

protected File javaFileDestDir = new File(new File("src", "main"), "java");
protected String javaFileEncoding = "UTF-8";
/* setter getter は省略 */

s2jdbc-gen-build.xmlに追加した属性です。
最初、getterはなくていいと思って書かなかったら怒られました(どうでもいいですね)。

protected static ThreadLocal<GenerateCommentCommand> self = new ThreadLocal<GenerateCommentCommand>();

@Override
protected void doExecute() throws Throwable {
	self.set(this);
    
	com.sun.tools.javadoc.Main.execute(new String[]{
		"-doclet",
		"org.seasar.extension.jdbc.gen.internal.command.GenerateCommentCommand",
		"-sourcepath",
		FileUtil.getCanonicalPath(javaFileDestDir),
		"-encoding",
		javaFileEncoding,
		"-subpackages",
		rootPackageName
	});
}

public static boolean start(RootDoc root) {
	self.get().createComment(root);
	return true;
}

まずdoExecute()が実行され、ThreadLocalに自分自身を格納します。
これは後続するMain.execute()のために必要な措置です。
Main.execute()はソースファイルのコメントを読み込むメソッドです。
docletオプションを指定しないならソースファイルのコメントを読み込んだ後に標準のDocletが呼び出され、おなじみのJavadocが生成されます。
docletオプションを指定した場合はソースファイルのコメントを読み込んだ後に指定したクラスのpublic static boolean start(RootDoc root)が呼び出され、その後の処理がこのメソッドに委ねられます。
しかし。
しょっぱいことにこのメソッドstaticなんです。
ここはぱくったGenerateDdlCommandの力を最大限発揮するために、start()が実行されたらすぐに処理をThreadLocalに格納したGenerateCommentCommandインスタンスに戻します。

protected void createComment(RootDoc root) {
    BufferedWriter out = null;
	try {
		File dir = ddlVersionDirectoryTree.getCurrentVersionDirectory().getCreateDirectory().createChild("050-user").asFile();
		dir.mkdir();
		File oFile = new File(dir, "comment.sql");
		out = new BufferedWriter(new OutputStreamWriter(
				FileOutputStreamUtil.create(oFile), Charset.forName(ddlFileEncoding)));
		
		List<EntityMeta> entityList = entityMetaReader.read();
		PackageDoc packageDoc = root.packageNamed(ClassUtil.concatName(rootPackageName, entityPackageName));
		
		for (EntityMeta entity: entityList) {
			ClassDoc classDoc = packageDoc.findClass(entity.getEntityClass().getSimpleName());
			String tableName = entity.getTableMeta().getFullName();
			// getCommentSqlはコメントSQLを組み立てます。
			out.write(getCommentSql("table", tableName, classDoc.commentText()));
			
			// createFieldDocMapは引数のクラスとその親クラスのフィールドをMapにして返します。
			Map<String, FieldDoc> fieldMap = createFieldDocMap(classDoc);
			
			for (int i=0; i<entity.getColumnPropertyMetaSize(); i++) {
				PropertyMeta property = entity.getColumnPropertyMeta(i);
				FieldDoc fieldDoc = fieldMap.get(property.getField().getName());
				String columnName = tableName + "." + property.getColumnMeta().getName();
				out.write(getCommentSql("column", columnName , fieldDoc.commentText()));
			}
		}
	} catch (IOException e) {
		throw new IORuntimeException(e);
	} finally {
		CloseableUtil.close(out);
	}
}

読み込んだソースのコメントをコメントSQLにして出力します。
entityMetaReaderがすばらしいです。
read()ってするだけでEntityとTableとPropertyとColumnの情報がまるわかりです。

protected Map<String, FieldDoc> createFieldDocMap(ClassDoc classDoc) {
	Map<String, FieldDoc> fieldMap = CollectionsUtil.newHashMap();
	for (FieldDoc fDoc: classDoc.fields()) {
		fieldMap.put(fDoc.name(), fDoc);
	}
	for (ClassDoc superDoc = classDoc.superclass(); 
			!Object.class.getName().equals(superDoc.qualifiedName()); superDoc = superDoc.superclass()) {
		if (isMappedSuperclass(superDoc)) {
			for (FieldDoc fDoc: superDoc.fields()) {
				fieldMap.put(fDoc.name(), fDoc);
			}
		}
	}
	return fieldMap;
}

引数のクラスとその親クラスのフィールドをMapにして返します。
ただし、@MappedSuperclassが付与されていない親クラスは除きます。

protected boolean isMappedSuperclass(ClassDoc classDoc) {
	for (AnnotationDesc desc: classDoc.annotations()) {
		if (MappedSuperclass.class.getName().equals(desc.annotationType().qualifiedName())) {
			return true;
		}
	}
	return false;
}

引数のクラスに@MappedSuperclassが付与されているかどうかを判断します。

protected String getCommentSql(String objectType, String objectName, String comment) {
	String commentOut = "";
	if(StringUtil.isBlank(comment)){
		comment = "";
		commentOut = "-- ";
	}
	return String.format("%scomment on %s %s is '%s';" + System.getProperty("line.separator"), 
			commentOut, objectType, objectName, comment); 
}

コメントSQLを組み立てます。

以上です。

entityMetaReaderのおかげですごくやりやすかったです。
gen-entityのほうは…どうしよう。