Tuesday, July 26, 2005

ทดลองเขียน Hibernate XDoclet 's Eclipse Plugin #3

สืบเนื่องจากการพยายามทำ Eclipse Plugin ที่ช่วย generate
Hibernate mapping file โดยใช้ XDoclet เป็น engine

หลังจากที่ทดสอบการเรียก XDoclet แบบไม่ต้องผ่าน Ant
และ patch เพื่อให้เรียก gen per Class ได้แล้ว
ขั้นถัดไปก็คือการ Implement Builder

เริ่มด้วยการ extends IncrementalProjectBuilder
และทำการ implement method build
protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
throws CoreException {
if (kind == IncrementalProjectBuilder.FULL_BUILD) {
fullBuild(monitor);
} else {
IResourceDelta delta = getDelta(getProject());
if (delta == null) {
fullBuild(monitor);
} else {
incrementalBuild(delta, monitor);
}
}
return null;
}


ตัว IResourceDelta ที่ได้มามีลักษณะเป็น hierarchy structure
ของ Resource ที่เกิดการเปลียนแปลง เช่นสมมติว่าใน project เรา
เจ้า file Server.java (package test) เกิดการเปลี่ยนแปลง
IResourceDelta ที่ได้มาก็จะมี structure ประมาณนี้

IResoucesDelta (src)
|
+--IResourceDelta (test)
|
+---IResourceDelta (Server.java)
|
+---IResourceDelta (Server.class)

Note: สังเกตุว่ามี class file มาด้วย
เพราะ eclipse บอกมันเป็น resource ที่เกิดการเปลี่ยนแปลงด้วย


โดยเราก็จะเลือกเฉพาะ leap node มาทำการ build
และกรองเอาเฉพาะพวกที่เป็น java file เท่านั้น
private void incrementalBuild(IResourceDelta delta, IProgressMonitor monitor) {
ArrayList list = new ArrayList();
getLeapNodes(list, delta);
for (Iterator iter = list.iterator(); iter.hasNext();) {
IResourceDelta rd = (IResourceDelta) iter.next();
if (rd.getProjectRelativePath().getFileExtension().equals("java")) {
docletBuild(rd);
}
}
}


ใน method docletBuild ก็จะ implement ดังนี้

เริ่มด้วยการพยายามหา path ที่จะเป็นตัวตั้งต้น
เพื่อให้ XJavaDoc ใช้ในการ solve หา Java Class ที่จะ parse
ประเด็นก็คือ เราจะหา source path ได้อย่างไร
เนื่องจากส่วนนี้ eclispe เปิดให้ user config ได้ตามใจชอบ

วิธีการหา source path ก็คือ ใช้ getRawClasspath()
ที่ return เป็น IClasspathEntry[] ซึ่งเราเลือกใช้เฉพาะ
พวกที่มีประเภทเป็น CPE_SOURCE


private IPath[] getSourcesPath(IJavaProject jproject)
throws JavaModelException {
ArrayList list = new ArrayList();
IClasspathEntry[] entries = jproject.getRawClasspath();
for (int i = 0; i < entries.length; i++) {

if (entries[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
list.add(entries[i].getPath());
}

}
return (IPath[]) list.toArray(new IPath[list.size()]);
}


ส่วนเจ้าตัว IJavaProject หาได้จาก
IResource res = resourceDelta.getResource();
IJavaProject jpr = JavaCore.create(rd.getResource().getProject());


พอได้ sourcepath มา ก็จัดการ add ให้ XJavaDoc
XJavaDoc xdoc = new XJavaDoc();
for (int i = 0; i < sourcePaths.length; i++) {
File dir = new File(rootPath.toString(), sourcePaths[i].toString());
FileSourceSet fs = new FileSourceSet(dir);
xdoc.addSourceSet(fs);
}


จากนั้นก็ใช้ sourcepath มาจัดการตัด path segment ของ
resource ที่เรากำลังจะ compile ให้เหลือเฉพาะ ส่วนที่เป็น package
directory เท่านั้น

IPath resPath = rd.getResource().getProjectRelativePath();
for (int i = 0; i < sourcePaths.length; i++) {
if (sourcePaths[i].removeFirstSegments(1).isPrefixOf(resPath)) {
int cnt = sourcePaths[i].segmentCount();
resPath = resPath.removeFirstSegments(cnt - 1);
resPath = resPath.removeFileExtension();
break;
}
}


จากนั้นก็เข้าสู่ขั้นตอนการใช้ XDoclet

String fullName = resPath.toString().replace('/', '.');
XClass cls = xdoc.getXClass(fullName);

HibernateSubTask sbt = new HibernateSubTask();
DocletContext ctx = new DocletContext("/tmp/gen", null, null,
new SubTask[] { sbt }, new Hashtable(), new HashMap(),
false, false, null);
DocletContext.setSingleInstance(ctx);
ctx.setActiveSubTask(sbt);

ModuleFinder.setClasspath(getPluginClasspath(rootPath));

sbt.init(xdoc);
sbt.generateForClass(cls);


ประเด็นที่เป็นปัญหา ก็คือ เจ้า XDoclet นั่้น
มีวิธีการ solve หา Tag Handler ใน Template
ด้วยวิธี scan XDoclet Module 's jar file ด้วยตัวเอง
ดังนั้นต้องมีการ set ค่า classpath ให้มัน โดยใช้ ModuleFinder
ในกรณีนี้ เราต้องหาว่า library ของเราอยู่ที่ path ไหน
จะได้ทำการ generate classpath string
ออกมาได้
Note: ถ้าดูใน source code ของ XDoclet
จะเห็นว่าเขาใช้ ModuleFinder.initClasspath(this.getClass())
ซึ่งข้างใน method initClassPath(Class clazz)
จะ solve หา classpath ดังนี้
classpath = ((AntClassLoader) clazz.getClassLoader()).getClasspath();
ซึ่งใน case นี้จะมาใช้กับกรณีของเราไม่ได้ เพราะเราไม่ได้ run ใน ant



Class ที่ใช้จัดการกับ plugin resources ก็คือ Bundle

private String getPluginClasspath(IPath rootPath) throws Exception {
Bundle bundle = HbdocletPlugin.getDefault().getBundle();

String requires = (String) bundle.getHeaders().get(
Constants.BUNDLE_CLASSPATH);
ManifestElement[] elements = ManifestElement.parseHeader(
Constants.BUNDLE_CLASSPATH, requires);

String bundleLoc = HbdocletPlugin.getDefault().getBundle()
.getLocation();
if (bundleLoc.startsWith("update@")) {
bundleLoc = bundleLoc.substring(7);
}
StringBuffer buff = new StringBuffer();
for (int i = 0; i < elements.length; i++) {
if (i > 0) {;
buff.append(File.pathSeparatorChar);
}
System.out.println(elements[i].getValue());
buff.append(rootPath.toString()).append(File.separatorChar);
buff.append(bundleLoc);
buff.append(elements[i].getValue());

}
return buff.toString();
}

Note: ตัว path ที่ bundle return กลับมา
ในกรณีที่ launch runtime instance จาก eclipse ในระหว่าง develop
จะมี prefix ที่น่าสนใจติดมาด้วย ก็คือ update@
ซึ่งคิดว่าตอนที่ deploy เป็น plugin จริงๆ
เจ้า prefix นี้คงหายไป


แค่นี้เราก็ได้ incremental xdoclet builder แล้ว
ส่วนกรณี full build ก็ทำเหมือนว่า run จาก ant ได้เลย

ไว้ตอนหน้าจะมาลอง customize เพิ่มให้สามารถ config
File Pattern, Hibernate Dtd version ได้

Related link from Roti

Sunday, July 24, 2005

ทดลองเขียน Hibernate XDoclet 's Eclipse Plugin #2

สืบเนื่องจากการพยายามทำ Eclipse Plugin ที่ช่วย generate
Hibernate mapping file โดยใช้ XDoclet เป็น engine ตอนที่ 1

หลังจากที่ลอง Implement Nature ไปแล้ว
ปัญหาถัดไปที่ต้อง solve ก็คือ
ทำอย่างไรจึงจะ run Doclet โดยไม่ใช้ Ant ได้
โชคดีที่ concept ของ Ant เปิดช่อง
ให้เราสามารถ run Ant task ต่างๆโดยตรงได้

โดยปกติเวลาเราเขียน ant task
สำหรับการ generate mapping file
เราจะเขียนด้วย syntax ดังนี้
<hibernatedoclet destDir="src/dao">
<fileset dir="src/dao">
<include name="mx/rd/web/model/**/*.java"/>
</fileset>

<hibernate version="2.0">
</hibernate>
</hibernatedoclet>

พอมาเขียนให้เรียกใช้จาก java ตรงๆ
ก็เลยเขียนเลียนแบบดังนี้
  
HibernateDocletTask task = new HibernateDocletTask();
task.setDestDir(new File("/tmp/xdoclet"));

HibernateSubTask subtask = new HibernateSubTask();
HibernateVersion ver = new HibernateVersion();
ver.setValue(HibernateVersion.HIBERNATE_3_0);
subtask.setVersion(ver);

task.addSubTask(subtask);

FileSet fs = new FileSet();
fs.setDir(new File("/tmp/xdoclet"));
fs.setIncludes("Role.java");

task.addFileset(fs);
task.setProject(new Project());

task.execute();

เมื่อทดลองเรียกใช้โดยตรง ก็เกิดปัญหาถัดไปว่า
ตัว Eclipse Builder ใช้ Concept Incremental Build
ซึ่งจะเลือก compile เฉพาะบาง file เท่านั้น
แต่เจ้าตัว XDoclet ใช้ concept Batch Build (เวลาทำ ทำเป็น set)
ถ้าเรา implement ตรงๆ Performance คงออกมาไม่ดีเท่าไร
เช่นสมมติว่าตัว Eclipse Builder
ระบุว่าต้องการให้ build file X.java แต่เจ้าตัว XDoclet
ดันไป scan project directory ทั้งหมด เพื่อที่จะได้เจ้าตัว
X.java มาตัวเดียว

ดังนั้นเราก็เลยพยายาม hack ต่อไป เพื่อที่จะหาว่า
จะสามารถเรียกใช้โดยตรงได้อย่างไรบ้าง
ก็ไปพบว่า มันมี method อยู่ตัวหนึ่งใน TemplateSubTask
(ซึ่งเป็น superclass ตัวหนึ่งของ HibernateSubTask)
ที่ชื่อ public void generateForClass(XClass clazz)
โชคไม่ดีที่ method นี้มี modifier เป็น protected
ทำให้เราไม่สามารถเรียกใช้ตรงๆได้
ก็เลยต้องใช้วิธีไม้ตายขั้นสุดท้าย
ก็คือเข้าไปแก้ไข source code ของ XDoclet โดยตรงเลย
โดยทำการเปลี่ยนจาก protected ให้เป็น public

Note ได้มีการทดลอง extends HibernateSubTask แล้ว override เปลี่ยน modifier ของ method generateForClass ตรงๆ แล้ว ปรากฎว่าไปเกิดปัญหาในเรื่อง namespace ของ template

เจ้า method generateForClass นี้ parameter ที่รับก็คือ XClass (อยู่ใน library ของ XJavaDoc ที่เป็นโครงสร้างพื้นฐานของ XDoclet)
สามารถหามาได้โดยวิธีนี้
    XJavaDoc xdoc = new XJavaDoc();
FileSourceSet fs = new FileSourceSet(new File("/tmp/xdoclet"));
xdoc.addSourceSet(fs);
XClass cls = xdoc.getXClass("mx.Role");


หลังจากทดลอง run ก็พบว่า ยังมี property หลายตัว
ที่ยังไม่ถูก initialize (เนื่องจากเราไม่ได้ run จากช่องทางปกติ)
ก็เลยต้อง initialize เพิ่มเติมดังนี้
    HibernateSubTask sbt = new HibernateSubTask();      

DocletContext ctx = new DocletContext("/tmp/gen",
null, null, new SubTask[] {sbt},
new Hashtable(), new HashMap(), false, false, null);
DocletContext.setSingleInstance(ctx);
ctx.setActiveSubTask(sbt);

ModuleFinder.initClasspath(getClass());
sbt.init(xdoc);


หลังจากเรียก initialize property ต่างๆแล้ว
ก็สามารถเรียก generate โดยตรงได้ดังนี้
    sbt.generateForClass(cls);

Related link from Roti