Thursday, August 30, 2007

ทดสอบต่อ RMI จาก Eclipse RCP ไป OFBiz

ประเด็นที่น่าสนใจ ก็คือ
  1. ปัญหาเรื่องการ pack Ofbiz library เฉพาะที่ client ต้องใช้ ให้เป็น Eclipse Plugin
  2. ปัญหาเรื่อง class loading


เริ่มที่อันแรกสุดก่อน
อันนี้ทำได้ง่ายๆ โดยการใช้ new wizard ของ Eclipse
ที่ชื่อ "Plugin from existing JAR Archives"
แต่ยังมีประเด็นที่ตาม ก็คือ library พวกนี้ ณ ขณะ runtime
มันมีการอ่าน properties file หรือ configuration file ด้วย
ดังนั้น เราเลยต้องสร้าง folder เพิ่มใน plugin
และกำหนดใน manifest file ให้มันเป็น classpath
จากนั้นก็ copy properties file ที่จำเป็นจาก ofbiz/framework/base/config
ซึ่งประกอบด้วย log4j.xml, jsse.properties, cache.properties

log4j.xml ถูกเรียกใช้จาก org.ofbiz.base.util.Debug lineno:86
ภายใน log4j.xml มี file appender อยู่จำนวนหนึ่งที่ระบุ file เป็น relative path อยู่
เนื่องจากตอนนี้ยังไม่อยากแตะประเด็นนี้ ก็เลยใช้วิธีแก้เปลี่ยนเป็น static path ไปไว้สักที่หนีงก่อน

jsse.properties ถูกเรียกใช้โดย org.ofbiz.base.util.SSLUtil lineno:249
เพื่อใช้หา configuration ของพวก proxyHost, proxyPort
ตัวต้นฉบับ ข้างในมีเนื้อหาที่ใช้จากฝั่ง server ด้วย
ซึ่ง client ไม่ต้องใช้, สามารถลบทิ้งทั้งหมดได้เลย

cache.properties ถูกเรียกใช้จาก org.ofbiz.base.util.cache.UtilCache lineno:217
ภายในมีการกำหนด relative path ของ disk cache file ด้วย
ก็ให้เปลี่ยนเป็น fix path ไปก่อน
(ตรงนี้ยังไม่ได้ดู แต่เข้าใจว่าสามารถลบทิ้งได้เยอะเหมือนกัน)

หน้าตา MANIFEST.MF file ของ plugin จะได้เป็นแบบนี้
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Rmiclient Plug-in
Bundle-SymbolicName: org.ofbiz.rmiclient
Bundle-Version: 1.0.0
Bundle-ClassPath: ofbiz-base.jar,
javolution-4.2.8.jar,
wsdl4j.jar,
ofbiz-service.jar,
jdbm-1.0.jar,
log4j-1.2.14.jar,
config/
Bundle-Vendor: com.orangegears
Bundle-Localization: plugin
Export-Package: .,
com.ibm.wsdl,
com.ibm.wsdl.extensions,
com.ibm.wsdl.extensions.http,
com.ibm.wsdl.extensions.mime,
...


จากตรงนี้ไป เราก็มี Ofbiz library ให้พร้อมเขียน RMI call แล้ว
ปัญหาถัดไปที่ตามมาก็คือ
เวลา run code นี้
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync("userLogin", UtilMisc.toMap("login.username",
connectionDetail.getUserId(),
"login.password",
connectionDetail.getPassword()));

มันจะเกิด error ดังนี้
java.rmi.UnmarshalException: error unmarshalling return; nested exception is: 
java.lang.ClassNotFoundException: org.ofbiz.entity.GenericValue
(no security manager: RMI class loader disabled)

เป็นประเด็นเรื่อง SecurityManager
แก้แบบ quick fix ด้วยการ implment Custom SecurityManager ลงไปก่อน
public class MySecurityManager extends SecurityManager {


public void checkPermission(Permission perm, Object context) {
}

public void checkPermission(Permission perm) {
}

}

จากนั้นก็แก้ code ข้างบนให้เป็นแบบนี้
System.setSecurityManager(new MySecurityManager());
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync(...);

Exception ตัวเก่า ก็จะหายไป ได้ exception ตัวใหม่มาแทน
java.lang.ExceptionInInitializerError
at org.ofbiz.base.util.UtilURL.fromOfbizHomePath(UtilURL.java:110)
at org.ofbiz.base.util.UtilURL.fromResource(UtilURL.java:76)
at org.ofbiz.base.util.UtilURL.fromResource(UtilURL.java:44)
at org.ofbiz.base.util.UtilProperties.getPropertyValue(UtilProperties.java:130)
at org.ofbiz.base.util.UtilProperties.getPropertyValue(UtilProperties.java:100)
at org.ofbiz.base.util.SSLUtil.loadJsseProperties(SSLUtil.java:249)
at org.ofbiz.base.util.SSLUtil.loadJsseProperties(SSLUtil.java:244)
at org.ofbiz.base.util.SSLUtil.(SSLUtil.java:52)
at org.ofbiz.service.rmi.socket.ssl.SSLClientSocketFactory.createSocket(SSLClientSocketFactory.java:42)

ถ้าลองเปิด code org.ofbiz.base.util.UtilURL ดูบริเวณ lineno 60-67 ซึ่งมีหน้าตาเป็นแบบนี้
        if (loader == null && url == null) {
try {
loader = Thread.currentThread().getContextClassLoader();
} catch (SecurityException e) {
UtilURL utilURL = new UtilURL();
loader = utilURL.getClass().getClassLoader();
}
}

ปัญหาจะอยู่ตรง Thread.currentThread().getContextClassLoader()
(ถือเป็นปํญหา classic ของคนเขียน Eclipse RCP ที่พยายามใช้ library ที่เคย work บน web app มาก่อน)
ตัว currentThread ก็คือ thread ที่เป็นตัว start eclipse
ซึ่งแน่นอนว่า classpath มัน ไม่รวมถึง classpath ของ plugin ด้วย

ทางแก้แบบ quick fix ที่ได้รับความนิยมสูง ก็คือ
ห่อ code ที่จะ call ofbiz ด้วยอันนี้เสีย
Thread thread = Thread.currentThread();
ClassLoader loader = thread.getContextClassLoader();
thread.setContextClassLoader(this.getClass().getClassLoader());
try {

System.setSecurityManager(new MySecurityManager());
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(connectionDetail.getServerUrl());
Map result = dispatcher.runSync("userLogin", UtilMisc.toMap("login.username",
connectionDetail.getUserId(),
"login.password",
connectionDetail.getPassword()));

} finally {
thread.setContextClassLoader(loader);
}


ปิดคดี

Related link from Roti

2 comments:

Anonymous said...

สุดยอดเลยครับพี่ ตอนนี้กำลังหาเงินมาประทังชีวิตอยู่จะได้เริ่มกเวันที่ 15 ก.ย. นี้ ส่วนงานที่ทำก็ไม่พ้น OFBiz ครับเดี่ยวจะเอามาเล่นให้ฟังว่าเอาไปทำอะไร ตอนนี้ผมกำลังถอดระหัส pos ของไทยเจ้าหนึ่งอยู่แบบว่าอยากได้แนวทางนะครับไม่มีเจตนาจะ copy idea

ข่า said...

เดี๋ยงผมจะลองศึกษาด้วยนะครับ กำลังหัดอยู่