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

Wednesday, August 29, 2007

ทดลองเขียน Hangman บน Dojo 0.9

Dojo 0.9 ออกใหม่มาแล้ว ก็เลยต้องลองสักหน่อย
แน่นอน ก็ต้องเป็นเกมส์ Hangman
จะได้เอาไปโชว์ในงาน CNUG ด้วย

โดยผมจะลอง implment โดยแยก object ในเกมส์ เป็น component ย่อยๆ
เพื่อจะได้ทดลอง feeling ของการทำ modular ใน Dojo

จัดแบ่ง component ออกเป็น

+ cnug
+ hangman
- Game.js // domain object
- Graphics.js // ส่วนแสดงรูป
- Unreveal.js // ส่วนแสดงคำที่ให้ทาย
- Letters.js // ส่วนแสดงตัวอักษรให้กดเลือก
+ assets // เก็บภาพ
- ...png

ขั้นแรกสุด ลองมองที่ภาพรวมก่อน เวลาเอา module มาประกอบกัน,
หน้าตาของ file hangman.html เฉพาะส่วน body จะเป็นแบบนี้
<body>
<center>
<h1>Hangman</h1>
<div dojoType="cnug.hangman.Graphics"></div>
<div id="unreveal" class="letters" ></div>
<br/>
<div id="letters1" class="letters" dojoType="cnug.hangman.Letters" chars="abcdefghijklm" ></div>
<div id="letters2" class="letters" dojoType="cnug.hangman.Letters" chars="nopqrstuvwxyz" ></div>
<br/>
<div id="button" dojoType="dijit.form.Button" onclick="newGame()">New Game</div>
</center>
</body>

จะเห็นว่าเรานำ component มาวางต่อกันดังนี้
Graphics
Unreveal
Letters
Letters
Button

ใน component ที่นำมาวางข้างบน แยกเป็นสองพวกตามวิธี initialize widget,
ก็คือ พวกที่ initialize โดยวิธี declarative (โดยการระบุ attribute dojoType ลงไป)
กับพวกที่ initialize โดยวิธี programing

ลองดู javascript code ส่วนที่ทำหน้าที่ initialize โปรแกรม
// register module path
dojo.registerModulePath("cnug","../../cnug");

// require component
dojo.require("dojo.parser");
dojo.require("cnug.hangman.Game");
dojo.require("cnug.hangman.Unreveal");
dojo.require("cnug.hangman.Letters");
dojo.require("cnug.hangman.Graphics");
dojo.require("dijit.form.Button");

// initialize
dojo.addOnLoad(function() {
game = new cnug.hangman.Game();
var unreveal = new cnug.hangman.Unreveal({game: game}, dojo.byId("unreveal"));
dojo.connect(dijit.byId("letters1"), "select", game, "guess");
dojo.connect(dijit.byId("letters2"), "select", game, "guess");
newGame();
});

การ registerModulePath จะช่วยให้เราสามารถจัดแยก code ที่เราเขียน
กับ code ที่ dojo provide มาให้ เพื่อให้สะดวกต่อการ maintain version control ของโปรเจค
ขั้นตอนการทำงานหลักๆ ก็คือ
  • new Game object
  • สร้าง unreveal Component โดยผ่านค่า game instance เข้าไปทาง constructor ของ Unreveal
  • เชื่อม event ระหว่าง Letters component เข้ากับ Game instance
    เพื่อที่เวลา user กดเลือกตัวอักษร, game instance จะได้ update state ตัวเอง
  • เรียก function newGame ที่จะทำหน้าที่ ajax call ไปขอคำศัพท์ จาก server
    และ set คำศัพท์นั้นให้กับ Game object


ลองมองเข้าไปใน Component แต่ละตัวบ้าง
เริ่มที่ Component Game ก่อน
หน้าตาของ Game.js เป็นแบบนี้
dojo.provide("cnug.hangman.Game");

dojo.declare("cnug.hangman.Game", null, {

newGame: function(word) {
this.word = word
this.unreveal = word.replace(/./g, '_')
this.mistake = 0;
this._fireNewGame();
},

guess: function(ch) {
var idx = this.word.indexOf(ch);
if (idx >= 0) {
var tmp = '';
for (var i = 0; i < this.word.length; i++) {
if (this.word.charAt(i) == ch) {
tmp += ch;
} else {
tmp += this.unreveal.charAt(i);
}
}
this.unreveal = tmp;
} else {
this.mistake++;
}
this._fireChange();
},

isWin: function() {
return this.unreveal.indexOf('_') < 0;
},

isLose: function() {
return this.mistake > 5;
},

_fireChange: function(){
dojo.publish("stateChange", [this]);
},

_fireNewGame: function(){
dojo.publish("newGame", [this]);
}
});

ประเด็นที่น่าสนใจก็มี
  • dojo.provide ก็คือคำสั่งที่ระบุว่า file นี้เป็น module อะไร
  • dojo.declare ก็คือ คำสั่งที่ใช้สร้าง Class Person (OOP ใน javascript เป็นพวก prototype base)
  • จะเห็นว่า method จะไม่มี Visiblilty scope กำกับ
    ทุกอย่างเป็น public หมด
    เราก็เลยใช้วิธีตั้งชื่อ อันไหนที่เป็น internal ก็นำหน้าด้วย underscore เสีย
  • การสื่อสารระหว่าง Game Object กับ component อื่นๆ
    ใช้ feature Publish-Subscribe ของ Dojo


ลองดู component ที่เป็น UI Widget บ้าง
เริ่มที่ Letters.js ที่ทำหน้าที่สร้างแถวตัวอักษรที่ user สามารถเลือก click ได้
และเมื่อ click แล้ว ตัวอักษรนั้น ก็จะ disabled ไม่ให้เลือกซ้ำอีก

ลองดูตัวเล็กสุดก่อน นั่นก็คือ LetterLink ที่ represent ตัวอักษรแต่ละตัว
dojo.declare("cnug.hangman.LetterLink", dijit.form._FormWidget,
{
char: '',

parent: null,

templateString: "<span><a href='#' dojoAttachPoint='anchor'
dojoAttachEvent='onclick: _select'>${char}</a> </span>"
,

_select: function() {
if (! this.disabled) {
this.setDisabled(true);
dojo.addClass(this.anchor, "disabledLink");
this.parent.select(this.char);
}
},

_reset: function() {
this.setDisabled(false);
dojo.removeClass(this.anchor, "disabledLink");
}
});

ประเด็นที่น่าสนใจ
  • วิธีการ declare Object ที่ extends จาก Object อื่นๆ
    เราสามารถใช้คำสั่ง dojo.delare("myclass", subclass, { // class body });
    หรือถ้า inherrit มาจากหลาย object ก็ใช้ dojo.delare("myclass", [subclass1,subclass2], { // class body });
    (ใช้คำว่า class เพื่อให้คนคุ้นกับ java เข้าใจ, แต่จริงๆมันคือ object หรือ function)
  • เราใช้ Object "dijit.form._FormWidget" เป็นต้นแบบ
    เนื่องจากมันมี คุณสมบัติหลายอย่างที่เราสามารถนำมาใช้ได้เลย เช่น disable, enable
  • templateString ก็คือการกำหนดว่าหน้าตาของ html ที่เรา generate ออกไปจะเป็นอย่างไร
    ใน dojo, เราสามารถแทรก attachPoint, attachEvent ใน template ได้
    อย่างใน code ข้างบน เรามี dojoAttachPoint เป็นค่า "anchor"
    หมายความว่า Dojo จะ inject DOM นั้นลงใน property ที่ชื่อ "anchor" ให้เราโดยอัตโนมัติ
    ทำให้เราสามารถเขียน this.anchor.src = 'xx' ได้เลย
    ส่วน dojoAttachEvent ทำให้เราร้อย event จาก DOM
    เข้ากับ function ใน Object เราได้ง่ายๆ

ย้อนกลับมาดู Letters ที่ทำหน้าที่เป็น container ของ LetterLink บ้าง
dojo.declare("cnug.hangman.Letters", [dijit._Widget, dijit._Container],
{
chars: '',

charsWidget: null,

postMixInProperties: function() {
this.charsWidget = [];
},

postCreate: function() {
var cs = this.chars.split('');
for (var i=0; i < cs.length; i++) {
this.charsWidget[i] = new cnug.hangman.LetterLink({char: cs[i], parent: this});
this.addChild(this.charsWidget[i]);
}
dojo.subscribe("newGame", this, "reset");
},

reset: function() {
dojo.forEach(this.charsWidget, function(elm) {
if (elm.disabled) {
console.debug("reset" + elm);
elm._reset();
}
});
},

select: function(ch) { console.debug(ch);}
});

ประเด็นน่าสนใจคือ
  • object นี้ inherrit จาก dijit._Widget และ dijit_Container
    คือเป็นทั้ง Widget ที่แสดงบนหน้าจอ และสามารถ container Widget ตัวอื่นๆได้
  • method ของ dijit._Widget ที่เรา override ก็คือ
    postMixinProperties ที่จะถูกเรียกหลังจากที่มีเสร็จสิ้นการ set property ต่างๆให้ object
    ส่วน postCreate ถูกเรียกใช้ เมื่อสิ้นสุดขบวนการสร้าง Widget แล้ว
  • select function เป็น function เปล่าๆ
    ใช้เป็นจุด extension point ให้สามารถร้อย function นี้
    เข้ากับ function ของ Object อื่นๆ ตามที่เราต้องการ
    (จะใช้กลไก publish-subscribe ก็ได้ แต่เบื่อแล้ว ก็เลยลองอย่างอื่นบ้าง)
  • พวก chars, charsWidget เป็น property ของ Object Letters
    ประเด็นที่ต้องระวังก็คือ พวก property ที่ชี้ไปยัง object (พวกที่เป็น by reference)
    จะต้อง initialize ใน postMixinProperties มิเช่นนั้น มันจะกลายเป็น
    static reference ที่ share กันระหว่าง object instance ของ Letters
    ลองดูตัวอย่างจาก code ง่ายๆนี้
    dojo.declare("Person",null, {
    item: [] // กลายเป็น static reference
    }
    p1 = new Person();
    p2 = new Person();
    p1.item[0] = 1;
    p2.item[1] = 2;
    alert(p1.item); // => [1,2]



ชักจะยาวไปแล้ว
ใครสนใจดูรายละเอียด ไปคุยกันได้ที่งาน CNUG ครับ

Note: ตอนแรกว่าจะลองเขียนด้วย YUI ด้วย
แต่พบว่า YUI เขียนเป็น modular แล้วไม่สนุกเท่า Dojo
ก็เลยเลิก

Related link from Roti

Monday, August 27, 2007

BTD3

ไปมาแล้ว ก็ควรเขียนถึงสักหน่อย

session ของคุณ apirak ได้ยินคนถามส่งเสียงเจื้อยแจ้วมาจากแถวหลัง
อดใจไม่ได้ต้องหันไปมอง น้อง Bact' เด็กช่างคิด(จะใช้คำว่าเด็กมีปัญหา ก็เกรงใจ) ของเรานี่เอง
เสียดายเลิกถกกันเร็วไปนิดเนอะ
น้อง art เราหนีกลับไปก่อน เลยไม่ได้คุยนอกรอบ

ผมถามน้องคนหนึ่งที่มาแสดง ubuntu ว่าเริ่มเล่น com ตั้งแต่เมื่อไร
น้องเขาตอบว่า "เริ่มเล่นตอน 4 ขวบ"

ตอนคุณสุกรี เดินไปเปิดห้อง grid
แอบเห็นลูกชายคุณสุกรีแว่บๆ
ก็เลยเดินเข้าไปคุยด้วย
ฮ้าๆ ได้เจอทั้งลูกทั้งแม่เด็กเลย
แฟนคุณสุกรี น่ารักนะ ขอชมหน่อย
แต่อาจจะตกใจไปนิด
แน่หล่ะเจอชายผิดดำ ร่างสูงใหญ่ หน้าตาเหี้ยมๆ หนวดไม่ได้โกน
เดินเข้าไปทักเอาดื้อๆ

เจอน้อง.. จำชื่อไม่ได้แล้ว (short term memory ผมไม่ดี)
ที่มากับอาจารย์มะนาวน่ะ
เห็นถือหนังสือ agile with rails ภาคภาษาไทย (ที่ไปลอกเขามาด้านๆ)
ก็เลยเข้าไปถามว่า "ดีไหม หนังสือเล่มนี้"
น้องเขาหันมายิ้มพราว
แล้วก็บอกว่า "เจอที่ตลกด้วย"
แล้วก็เปิดให้ดู
มันเป็นบทที่ว่าด้วย การ install rails
เขาเขียนประมาณว่า
"โปรแกรม dave เป็นโปรแกรมที่ใช้ในการติดตั้ง rails
โดยโปรแกรมนี้มีทั้งบน linux, mac และ windows"
อ่านจบคิ้วเริ่มขมวด โปรแกรมอะไรวะ ไม่เคยได้ยิน
กวาดสายตามาดู source code ก็เลยบางอ้อ
dave> tar xzf ruby-x.y.z.tar.gz
dave> cd ruby-x.y.z

ตายๆ unix prompt ของ Dave Thomas กลายเป็นโปรแกรม dave ไปได้

ตกเย็นไปกินข้าวต่อ
ได้คุยกับแฟนน้อง keng
คลื่นสมองตรงกัน ถูกคอมาก
ก่อนกลับเลยแวะบอก keng ว่า
"เก็บเงินแต่งงานได้แล้วนะ"
กลับมาบ้าน ก็มาเล่าให้ภรรยาฟัง
คุณภรรยาถามสั้นๆ "ผู้หญิงหรือผู้ชายน่ะ"

งานนี้ น้องๆส่วนใหญ่ที่เคยเจอ
หลายคนดูแก่ขึ้น หรือไม่ก็ดูเหนื่อยๆ
เช่น Wiennat(เพราะกำลังสอบอยู่),
Rerngrit(เมื่อคืนขี่จักรยานไกลและดึกไปหน่อย)
ที่เห็นว่าดูหนุ่มขึ้น ก็เห็นมีเจ้า keng คนเดียว (เทียบกับ งาน tlug)

Related link from Roti