Thursday, October 11, 2007

Up Function (revise)

จาก Post ก่อน Link
ด้วยความรีบร้อน มุ่งแต่ function เลยไม่ได้สนใจ form
ให้ดีแล้วแล้วควรจะเขียนแบบนี้
ดูเป็นกลุ่มก้อนกว่า
dojo.provide("orangegears.dom.Util");

orangegears.dom.Util = {

up: function(node, expression) {
var ancestors = this.ancestors(node);
for (var i = 0; i < ancestors.length; i++) {
var tmp = dojo.query(expression, ancestors[i]);
if (tmp.length > 0) {
return tmp[0];
}
}
return [];
},

ancestors: function(node) {
return this.recursivelyCollect(node, "parentNode");
},

recursivelyCollect: function(element, property) {
var elements = [];
while (element = element[property])
if (element.nodeType == 1)
elements.push(element);
return elements;
}

}

Related link from Roti

Up Function

ใน Prototype (javascript library) มันมี function อยู่อันหนึ่งที่ผมชอบ ก็คือ "up"
function นี้ช่วยให้เรา search หา parent element ได้ง่ายขึ้น
ช่วยให้ลดความซับซ้อนของบาง feature ลง

ลองดูตัวอย่างการใช้แบบง่ายๆ
  <ul>
<li>xxx</li>
<li>yyy</li>
<li class="item">
zzz
<!-- กดแล้วจะ remove item ที่เป็นพ่อของปุ่มนี้ออก -->
<input type='button' value="remove"
onclick='this.up(".item").remove()'/>
</li>
</ul>

ตัวอย่างนี้ simple มากจนไม่จำเป็นต้องใช้ function up ช่วยก็ได้
แต่ใน app ที่เป็นพวก Rich client ตัว DOM จะซ้อนกันสลับซับซ้อนกว่ากว่าตัวอย่างนี้มาก
มี up ก็ช่วยให้เราทำอะไรหลายอย่างได้ง่ายขึ้น

แต่ถึงจะชอบอย่างไร, project ปัจจุบันผม ก็ไม่ได้ใช้ Prototype แต่ใช้ Dojo,
ซึ่งลองความหาใน code แล้วยังหา function แบบนี้ไม่เจอ (code base มันใหญ่, อาจจะมีอยู่แล้วแต่หาไม่เจอ)
ดังนั้นปฏิบัติการลอกเลียนแบบ ก็เลยเกิดขึ้นดังนี้
dojo.provide("orangegears.dom.Util");

orangegears.dom.Util.up = function(node, expression) {
var ancestors = orangegears.dom.Util.ancestors(node);
for (var i = 0; i < ancestors.length; i++) {
var tmp = dojo.query(expression, ancestors[i]);
if (tmp.length > 0) {
return tmp[0];
}
}
return [];
}

orangegears.dom.Util.ancestors = function(node) {
return orangegears.dom.Util.recursivelyCollect(node, "parentNode");
}

orangegears.dom.Util.recursivelyCollect = function(element, property) {
var elements = [];
while (element = element[property])
if (element.nodeType == 1)
elements.push(element);
return elements;
}

namespace ยาวไปนิด แต่ไว้ไปสร้าง shortcut ทีหลังได้
ตัวอย่างเวลานำไปใช้จริง
dijit.byId(orangegears.dom.Util.up(this, '.dojoxGrid').id).setAll(this.checked);

Related link from Roti

Tuesday, October 09, 2007

Shadow




They see only their own shadows, or the shadows of one another, which the fire throws on the opposite wall of the cave?
Socrates
The Allegory of the Cave (The Republic , Book VII)


งานของ Tim Noble and Sue Webster, Dirty White Trash [With Gulls] 1998

Note: รูปจาก Freeze Frame Screen, the Shadow, Hot Heads Under Silent Wigs

Related link from Roti

Friday, October 05, 2007

java กับ erlang

วันก่อนเห็น java guy บ่นเรื่อง Erlang
แต่ก็ไม่ได้สนใจอ่าน
(คนอะไร ยังไม่ทันมีประสบการณ์เลย บ่นถึงข้อเสียได้เป็นฉากๆ)

วันนี้เห็น Aristotle Pagaltzis เขาเขียนล้อเลียนสั้นๆ
Plus ça change
มีอารมณ์ชัน และเสียดสีดี

Related link from Roti

Thursday, October 04, 2007

ประกันสังคม

อาจารย์ผมเขียนเกี่ยวกับประกันสังคมไว้ใน mail ของบริษัทฯ
เนื้อหามันไม่ได้จำกัดอยู่กับคนในบริษัทฯ ก็เลยคัดลอกมาให้อ่านกัน

ผู้ใช้แรงงาน(รวมมนุษย์เงินเดือนบางพวก) เป็นผู้มีฐานะทางสังคมระดับล่าง มีจำนวนมาก รวมกันไม่ติด อ่อนแอ ไม่มีอำนาจต่อรอง

ระบบประกันสังคม เป็นระบบลงขันตามสัดส่วนรายได้ แล้วจ่ายคืนในรูปสวัสติการพื้นฐาน เช่น รักษาพยาบาล

หลอกให้เชื่อว่า ให้หลักประกันชราภาพ (แต่สะสมเงินสมทบแบบจำกัด จึงจ่ายคืนแบบจำกัด) ทั้ง ๆ ที่ไม่สามารถดำรงชีวิตอยู่ได้จากเงินรับกรณีชราภาพ แล้วถ้าคำนวณเป็น จะรู้ว่า เงินกองทุนชราภาพจะไม่พอจ่าย เพราะ (สมทบเดือนละ 1.5% + อีกเท่าตัวจากนายจ้าง แล้วได้รับบำนาญต่อเดือน 15% ของเฉลี่ย 60 เดือนสุดท้าย สูงสุด 2,250 บาท) ส่งสมทบ 1 ปี จ่ายคืนได้แค่ 2 เดือนกว่าก็หมด ทำงานมา 30 ปี จ่ายคืนได้ 72 เดือน(6 ปี) ส่วนที่สมทบไว้หมดแล้ว ดอกผลไม่มากพอ แต่คนอายุยืนกว่า 61 ปี (สิทธิชราภาพ เริ่มได้เมื่อไม่ทำงานหลังอายุครบ 55 ปี)

เรื่องประกันสังคม ต่อไปจะเป็นปัญหาสังคม เมื่อกองทุนไม่มั่นคง แต่ตอนนี้ปัญหายังซุกตัวอยู่ใน ภูเขาน้ำแข็ง คนส่วนใหญ่เห็นเม็ดเงินสะสมมูลค่ามาก เพราะ สิทธิประโยชน์ชราภาพยังไม่เกิดเต็มที่ คนที่เกษียณตอนนี้ กรณีสมทบไม่เกิน 12 เดือน จะได้รับบำเน็จเอาเงินก้อนที่สมทบไว้ คืนกลับไปบวกดอกเบี้ยนิดหน่อย (ถ้าสมทบเกิน 12 เดือน แต่ไม่ถึง 180 เดือน ได้คืนส่วนของนายจ้างด้วย) ปลายปี 2556 จึงจะเริ่มได้สิทธิบำนาญ เมื่อถึงเวลานั้น เงินกองทุนชราภาพ จะเริ่มหดหาย ซึ่งอาจจะยังไม่รู้สึก เพราะ เงินเข้ายังมากกว่าเงินออก ต้องผ่านไป 10 ปี 20 ปี เมื่อคนเกษียณมากขึ้น แล้วไม่ตายง่าย จนหมุนเงินเข้าไม่ทันเงินออก จึงจะเริ่มเห็นอาการเงินกองทุนหด แต่เพราะเวลายังอีกนาน จะยังไม่มีใครร้อนตัวตอนนี้ พวกลูกจ้างจะเรียกร้องสิทธิประโยชน์เพิ่ม โรงพยาบาลจะเก็บเงินเพิ่ม พวกเหลือบจะมาดูดเงินจากกองทุน

ผมอายุยืนไม่ถึง ที่จะเห็นปัญหาปูดขึ้นมา

การประกันสังคม เป็นลักษณะการออมแบบหนึ่ง (กองทุนสำรองเลี้ยงชีพ ก็เช่นเดียวกัน) แต่การออมแบบนี้ บรรเทาความวิกฤตทางสังคมได้หน่อยหนึ่ง ไม่พอเลี้ยงชีพเมื่อยามแก่เฒ่า

ถ้าผู้ใช้แรงงานเป็นคนแก่ ไม่มีลูกหลานเลี้ยงดู ก็อยู่ไม่ได้ ถ้าลูกหลาน ยังไม่เปลี่ยนฐานะจากผู้ใช้แรงงาน เป็นฐานะอื่นที่ดีกว่า ก็อาจดูแลได้จำกัด เพราะ ลำพังตัวผู้ใช้แรงงาน ดูแลตัวเอง ก็ลำบาก

ยุคอุตสาหกรรม ผู้ใช้แรงงานในโรงงาน เป็นผู้หนุนความมั่งคั่งให้นายทุน

Knowledge worker คือ ผู้มีการศึกษา ต่างจากผู้ใช้แรงงานในโรงงาน พวกนี้มีพัฒนาการสูงกว่า (แต่สำหรับประเทศไทย เราขยับตัวด้านการศึกษาช้าเกินไป เหมือนกับเป็นคนหลับ ถึงตอนนี้ ก็ยังไม่ตื่น สะลึมสะลือ งัวเงีย เราหลงผิดว่าเราทำดีที่สุดแล้ว เพราะ ประเทศเราอุดมสมบูรณ์ เราจึงไม่ค่อยกระตือรือร้น )

ในส่วนที่เกี่ยวกับประกันสังคม บริษัทของเราจ่ายเงินสมทบ มากกว่ารับประโยชน์คืน พนักงานส่วนใหญ่เป็น Knowledge worker แต่พวกเราบางคนอาจจะยังไม่ตระหนักว่า เราจะต้องรู้จักออมมากกว่าที่เป็นอยู่ สังคมรอบข้าง มีวัตถุหลอกล่อให้เราหลงไหลง่าย เราต้องรู้จักอดกลั้นต่อสิ่งยั่วกิเลส ยั่วความฟุ้งเฟ้อ เราต้องนึกถึงวันข้างหน้าที่อาจไม่แน่นอน ต้องเผื่อเหนียว กลัวอด เราจะไปหวังพึ่งเงินบำนาญชราภาพจากประกันสังคมไม่ได้ อนาคตอยู่ในมือเราเอง แต่เราร่วมมือกันหนักแน่น จะแข็งแรงกว่าคนเดียวหรือจับมือแบบหลวม ๆ

ดร. สุริยัน ติษยาฐิคม

Related link from Roti

Wednesday, October 03, 2007

Dojo Tree

การใช้ dijit.Tree
ใน dojo0.9, มีการ re-design Tree Widget ใหม่
โดย copy code เดิมมาจาก TreeV3 ใน dojo 0.4
แล้วปรับเปลี่ยนวิธี define item ใน tree เสียใหม่
โดยเปลี่ยนไปใช้ abstraction ที่ชื่อ Store
เป็นผู้ทำหน้าที่ provide data ให้กับ Tree

ลองดู use case แบบง่ายสุดก่อน
เริ่มด้วยการ define Data ที่จะใช้เป็น model ในการสร้าง Tree
      var treeData = {
label: "name",
identifier: "id",
items: [
{ id: 1, name: "Micro-X", children: [
{ id: 3, name: "Developement" },
{ id: 4, name: "R&D" }
] },
{ id: 2, name: "Ingres", children: [
{ id: 5, name: "Marketing" }
] }
]
}

var store = new dojo.data.ItemFileReadStore({ data: treeData });

Attributes หลักๆที่ Store ต้องการก็คือ
  • label จะใช้กำหนดว่า เวลา render Tree Node แล้ว
    ค่าที่แสดงเป็น label จะนำมาจาก attribute ที่ชื่ออะไร
  • identifier ใช้กำหนดว่า attribute name ไหน represent unique key ใน dataset
  • items ตัวเนื้อ data ที่ represent tree node

เมื่อ define Store แล้ว ก็สามารถนำไป feed ให้ Tree Widget ของเราได้
    <div dojoType="dijit.Tree" store="store" id="tree" label="Company">
</div>


แค่นี้ก็ได้ Tree หน้าตาแบบนี้ออกมา



ตัว Tree Widget นี้ มันมี extension point ให้เรา extend functional หลายตัว เช่น
ถ้าเราต้องการ ดัก click event
เราสามารถ override หรือ connect onClick ได้ดังนี้
    <div dojoType="dijit.Tree" store="store" id="tree" label="Company">
<script type="dojo/connect" event="onClick" args="item">
console.debug(item.name);
var name = store.getValue(item, "name");
console.debug(name);
</script>
</div>

ปัญหาก็คือ ตัว Store มันเปลี่ยนรูป Data ที่เรา feed เข้าไป (นัยว่าเพื่อ optimize อะไรบางอย่าง)
ถ้าเราใช้ item.name เพื่อดึงค่าขึ้นมา, ค่าที่ได้จะได้ datatype เป็น array แทนที่จะเป็น string ธรรมดา (ตามที่เรา declare ไว้)
ดังนั้นก่อนที่จะนำค่าจาก item ไปใช้ ก็เลยต้องมี wrapper code นี้พอกลงแบบนี้
store.getValue(item, "name")
Note: อันนี้นับว่าเป็นข้อเสียของ Tree api นี้อย่างมาก, implementation ควรจะซ่อนให้พ้นสายตาจากคนใช้

จุด extension อื่นๆ ก็มีอย่างเช่น
ถ้าต้องการ customize Label
    <div dojoType="dijit.Tree" store="store2" id="tree2" label="Company">
<script type="dojo/method" event="getLabel" args="item">
return store2.getValue(item, "name") + "_hi";
</script>
</div>

ข้อจำกัดอีกอย่างของ Tree Label ก็คือ มันแสดงได้เฉพาะ Plain Text เท่านั้น
ถ้าต้องการอะไรที่ดูดีขึ้น เช่นเปลี่ยนสี
ก็ให้ไปใช้ css class ช่วยแทน โดยทำการ override method getLabelClass
    <div dojoType="dijit.Tree" store="store4" id="tree4" label="Company" childrenAttr="children,staffs">
<script type="dojo/method" event="getLabelClass" args="item">
if (item) {
if (this.store.getValue(item, "type") == 'employee') {
return "employee";
}
}
return "";
</script>
</div>

Note: ใครที่จะ test ตรงนี้ ให้ระวังว่า ช่วงนี้มีการ refactor อยู่
ทำให้ code ในส่วนนี้หายไปจาก trunk, ผมกำลังรอ submit patch อยู่

ประเด็นที่น่าสนใจของ tree model ก็คือ เราสามารถ share node ได้
โดยการใช้ _reference attribute
      var treeData2 = {
label: "name",
identifier: "id",
items: [
{ id: 1, name: "Micro-X", children: [
{ id: 3, name: "Developement" },
{ id: 4, name: "R&D" }
] },
{ id: 2, name: "Ingres", children: [
{ _reference: { id: 3 }},
{ id: 5, name: "Marketing" }
] }
]
}

node "Development" ก็จะถูก share ใช้ร่วมกัน
แต่ข้อสงสัยที่ตามมาก็คือ use case ไหน? ที่มันจำเป็นต้องทำแบบนี้

นอกจากนี้ยังมี feature แปลกๆอีกเช่น
เราสามารถกำหนด attribute name ของ child element ได้มากกว่า 1 (default คือ "children")
เช่น data ชุดนี้ มี child element อยู่ 2 แบบ (children และ staffs)
เข้าใจว่า feature นี้มีไว้ สำหรับกรณี Tree ที่ mix content จากหลายๆ class
      var treeData4 = {
label: "name",
identifier: "id",
items: [
{ type: "company", id: 1, name: "Micro-X", children: [
{ type: "department",
id: 3, name: "Developement",
staffs: [
{type: "employee", id: 100, name: "pok"}
]
},
{ type: "department",
id: 4, name: "R&D",
staffs: [
{type: "employee", id: 101, name: "kob"}
]
}
] },
{ type: "company", id: 2, name: "Ingres", children: [
{ _reference: { id: 3 }},
{ type: "department", id: 5, name: "Marketing" }
] }
]
}

เวลาใช้ เราก็ต้องกำหนด childrenAttr ให้ Tree ด้วย
    <div dojoType="dijit.Tree" store="store3" id="tree3" label="Company" childrenAttr="children,staffs">
</div>

Related link from Roti

Monday, October 01, 2007

Dojo ???

ท่ามกลางทะเล framework และ library ที่มากมาย
ในยุค information overload นี้
สิ่งที่มักจะเกิดในใจของผมก็คือ
"เราเลือกใช้ถูกตัวหรือเปล่า"
"มีตัวอื่นที่ดีกว่านี้หรือเปล่า"

ตอนที่ใช้ Dojo ก็เช่นกัน
เลือกเพราะอ่าน code แล้วมันติดใจ, เขียน code แล้วสนุก
ใช้ไปนานๆเข้า
framework ตัวอื่นๆก็เริ่ม เตะตา (เตะบ่อยด้วย)
JQuery ก็มีคนชมเยอะ
Ext ก็สวยเสียเหลือเกิน
...

แต่ความรู้สึกพวกนี้ ก็ลดลงได้
เวลาเจอคนอื่นที่ใช้เหมือนเรา
อย่างเดือนที่ผ่านมา
อย่างน้อยก็เจอ site ใหญ่ที่ใช้ dojo เพิ่มขึ้น
  • beta.bloglines.com -> ใช้ dojo0.9 แต่โทษที UI ไม่สวยเลย
  • store.apple.com -> ใช้ dojo รุ่นโบราณมาก 0.4.0
    แถมยัง load library อะไรมาเยอะแยะไปหมดเลย ซึ่งแต่ละหน้าใช้จริงๆไม่เท่าไร
    คนเขียนเขาไม่ได้ใช้ custom build ซึ่งจะช่วยลดจำนวน file ที่ต้อง load ลง

Related link from Roti

Thursday, September 27, 2007

Tapestry กับ Wicket

หลายคนคงสงสัยว่า เจ้าสองตัวนี้มันมีอะไรที่ต่างกัน
Keng Tong (ซึ่งเป็น committer ของ Tapestry, และเป็นคนเขียนหนังสือ Enjoy Tapestry)
เขาเฉลยให้ฟังแล้ว

My thoughts on the differences between Tapestry and Wicket

I've never seen any code that is as good quality as the Tapestry code

เห็นด้วยว่า code ของ Tapestry นี่มันดีจริงๆ

Wicket tend to have more working functions than Tapestry (tree component, more ajax functions, modal dialog, dynamic images, wizard. role based authorization, captcha, breadcrumb, ...).

ใช่เลย อย่าลืมตามไปอ่านเหตุผลด้วยว่า ทำไม...

Related link from Roti

Wednesday, September 26, 2007

ประสบการณ์ Dojo 0.9

ช่วงนี้กำลังนั่งทำ Orangegears กับน้อง sand เสียเป็นส่วนใหญ่
สิ่งที่ผมต้องการสร้างความแตกต่างระหว่าง Ofbiz กับ Orangegears ก็คือ UI
ช่วงนี้ก็เลยนั่งลอง prototype ความเป็นไปได้ในการใช้ Dojo มาเป็น layer ในส่วน UI
ซึ่งก็พบความแตกต่างระหว่าง 0.9 และ 0.4 จำนวนหนึ่ง
ก็เลยมา blog ไว้

เริ่มด้วยความแตกต่างระหว่าง 0.4 กับ 0.9 ที่สัมผัสได้
  1. ความเร็วในการ parse
    เดิมใน Dojo 0.4 ปัญหาที่เราพบก็คือ dojo.parser ทำงานช้า
    ในกรณีที่เรามี DOM tree ที่ใหญ่มากๆ, dojo.parser จะทำงานช้าในแบบที่รับไม่ได้
    (ซึ่งมันมีวิธีการแก้ไข โดยการ scope ส่วนที่ dojo.parser ทำงานให้มีขนาดเล็กลง)
    ใน Dojo 0.9 ความเร็วในการ parse เพิ่มขึ้นอย่างมาก
    เข้าใจว่าได้แรงบันดาลใจมาจาก JQuery ซึ่งสามารถ scan DOM ได้เร็วมากๆ

    ผลพลอยได้จากการที่ parser มันเร็ว ก็คือ style การเขียนที่เปลี่ยนไป
    แนวทางการเขียนแบบ declarative ถูก encourage มากขึ้น
    ส่งผลให้ code ดูสวยและอ่านง่ายขึ้น (สำหรับ developer ที่สายตาคุ้นกับ xml สไตล์ flex, xul แล้ว)

    ข้างล่างคือตัวอย่าง code ที่ผมลองเขียนใน Orangegears ดู
    เป็นหน้าจอที่แสดง Tree ของผังบัญชี
    style การเขียนเป็นแบบ declarative
    <!-- Store น่าจะเปรียบได้กับพวก DataProvider ใน flex-->
    <div dojoType="dojo.data.ItemFileReadStore"
    jsId="financial.accountCharts"
    url = "getAccountTreeJson">
    </div>

    <!-- Tree Widget -->
    <div id="accountCharts"
    dojoType="dijit.Tree"
    store="financial.accountCharts" <!-- link เข้าหา DataProvider ข้างบน -->
    label="Accounts">

    <!-- เชื่อม script นี้เข้ากับ event onClick เข้า Tree widget นี้
    สังเกตว่า type มีค่าเป็น dojo/connect
    -->
    <script type="dojo/connect" event="onClick" args="item">
    var glAccountId = financial.accountCharts.getValue(item, "glAccountId");
    dojo.xhrGet({
    url: "getGlAccountJson",
    handleAs: "json",
    content: {glAccountId: glAccountId},
    load: function(obj, ioArgs) {
    dijit.byId("glAccountTypeId").setValue("");
    dijit.byId("glAccountClassId").setValue("");
    dijit.byId("glAccountForm").setValues(obj);
    }
    });
    </script>

    </div>

  2. Layout Widget
    ใน Dojo 0.9 มันมี Layout Widget เพิ่มขึ้นมา
    ทำให้การสร้าง App สะดวกขึ้นอย่างเห็นได้ชัด
    ทำให้เราเป็นอิสระจาก CSS มากขึ้น (ไม่งั้นก็มัวไปนั่ง control CSS ให้ได้รูปแบบตามที่เราต้องการ)


ในเรื่องของ source code ของ Dojo
style การเขียนส่วนใหญ่ยังคล้ายคลึงกับ 0.4 อยู่
ถ้าไล่ code 0.4 เป็น ก็สามารถใช้ 0.9 ได้เลย
โดยใน 0.9 นี้, dojo rewrite widget system ใหม่ในชื่อ package ว่า dijit
ผลจากการ rewrite ก็ทำให้จำนวน object, function ในส่วน Widget มันลดลง เป็นระบบระเบียบมากขึ้น
การไล่ code ที่เกี่ยวกับ UI ก็เลยทำได้ง่ายขึ้น
(บ้างอย่างก็ง่ายขึ้น, บางอย่างก็งงๆ เพราะมีความเป็น generic มากไปหน่อย)

Bug ใน Dojo 0.9
เท่าที่ใช้มาพึ่งเจอแค่ bug ตัวเดียวซึ่งก็ร้ายแรงอยู่
เป็นเรื่องเกี่ยวกับ การ reload content ใน ContentPane
ซึ่งแก้ไขโดยเปลี่ยนไปใช้ code จาก SVN โดยตรง
(หนีเสือ แต่อาจจะไปเจอจระเข้ เพราะมีความเสี่ยงมากขึ้นกับ bug ตัวใหม่ๆแทน)

Related link from Roti

Thursday, September 20, 2007

Eclipse oAW

กลับจากงาน NJUG4 ด้วยความค้างคาใจในเรื่อง MDA
ด้วยความที่ Robert ไม่ยอมแสดงตัวอย่างเจ๋งๆใน MagicDraw ให้ดู

เมื่อวันก่อนอ่านพบใน Eclipse Planet ว่ามี project 's annoucement ที่ชื่อ OpenArchitecture Ware (oAW)
ซึ่งเป็น Framework สำหรับ MDA ที่ integrate เข้ากับพวก MDD projects ของ Eclipse (เช่น EMF)
ก็เลยได้ฤกษ์ทดลองเสียเลย

มาลองดู step ของการใช้ oAW
ตั้งแต่ define MetaModel ไปจนถึง การ generate Source code

  • step แรกเริ่มต้นกันที่ การสร้าง MetaModel ก่อน (M2)
    ที่มาของ MetaModel ใน oAW มีได้ 2 แบบหลักๆคือ
    1. จาก EMF
    เมื่อก่อนเคยสงสัยมานานแล้วว่า EMF ไว้ทำอะไร
    หลังจากไปฟัง Robert พูดมา และกลับมาอ่านเอกสารของ EMF
    ทำให้เริ่มมองภาพ EMF ออกมากขึ้น
    ตัว EMF จริงๆแล้วก็คือ Implementation ตัวหนึ่งของ MOF (Meta Object Facility)
    แต่ implement แค่ subset บางส่วน, และเพื่อไม่ให้สับสนกับ MOF
    ส่วน subset model ที่ EMF implement เขาก็เลยตั้งชื่อว่า Ecore
    (ปัจจุบัน MOF 2.0, subset ที่ EMF support เขาตั้งชื่อว่า EMOF)

    วิธีสร้าง MetaModel ก็มีหลายแบบ เช่น
    • define มันตรงๆเลย ด้วย Editor ของ EMF
    • export มาจากพวก UML Tool ที่ support XMI format
    • model ด้วย Java Code + Annotation
    • ใช้ XSD ในการ define


    2. สร้างจาก xText
    อันนี้น่าสนกว่าวิธีที่ 1 เยอะ
    ถ้าอ่าน overview ของ xText จะเห็นว่า
    เขาประกาศตัวเป็น Textual DSL development framework
    การสร้าง MetaModel ใน xText ก็จะใช้การเขียน Grammar rules แทน

    ตัวอย่างของ MetaModel ที่ oAW ใช้ใน tutorial ก็มีหน้าตาแบบนี้
    เป็นตัวอย่างของการ define MetaModel ของ Simple Class Diagram



  • ขั้นตอนถัดไป ก็คือการสร้าง Model (ที่เป็น instance ของ MetaModel ที่ได้จากขั้นตอนแรกของเรา)
    ถ้าเราใช้ MetaModel จาก EMF ในขั้นนี้ เราเลือกทางเลือกได้หลายทาง
    เช่น ให้ EMF generate Editor ให้เรา เพื่อที่เราจะได้ใช้ Editor ตัวนั้นสร้าง Model
    หรือจะไปพวกตระกูล GMF สร้าง Graphic Editor มาเพื่อที่จะมาสร้าง Model ก็ได้

    แต่ถ้าเลือก choice ของ xText
    ในขั้นนี้ เราก็จะให้ xText generate Text Editor ให้เรา
    แล้วเราก็ใช้ Text Editor นั้น เขียน DSL ของเราได้เลย

  • ขั้นถัดไป ก็คือการ กำหนด build script ที่ใช้ในการ generate
    oAW เรียก script พวกนี้ว่า Workflow
    ตัวนี้ดูแล้ว ได้อารมณ์เหมือนเขียน Ant เลย
    <workflow>
    <property file="workflow.properties"/>

    <!-- ให้ parser อ่าน Model ของเรา โดยอ้างอิง MetaModel ด้วย -->
    <component id="xmiParser"
    class="org.openarchitectureware.emf.XmiReader">
    <modelFile value="${modelFile}"/>
    <metaModelPackage value="data.DataPackage"/>
    <outputSlot value="model"/>
    <firstElementOnly value="true"/>
    </component>

    <!-- validate model โดยใช้พวก OCL script -->
    <component
    class="org.openarchitectureware.check.CheckComponent">
    <metaModel id="mm" class="org.openarchitectureware.type.emf.EmfMetaModel">
    <metaModelPackage value="data.DataPackage"/>
    </metaModel>
    <checkFile value="checks"/>
    <emfAllChildrenSlot value="model"/>
    </component>

    <!-- ลบ source file ที่เคยถูก generate ไว้ -->
    <component id="dirCleaner"
    class="org.openarchitectureware.workflow.common.DirectoryCleaner" >
    <directories value="${srcGenPath}"/>
    </component>

    <!-- generate file โดยใช้ template ที่ระบุ -->
    <component id="generator"
    class="org.openarchitectureware.xpand2.Generator">

    <metaModel id="mm"
    class="org.openarchitectureware.type.emf.EmfMetaModel">
    <metaModelPackage value="data.DataPackage"/>
    </metaModel>

    <expand value="templates::Root::Root FOR model"/>

    <outlet path="${srcGenPath}/">
    <postprocessor
    class="org.openarchitectureware.xpand2.output.JavaBeautifier"/>
    </outlet>

    </component>
    </workflow>


  • ในการ generate source code สิ่งที่ต้องมีก็คือ Template Engine
    หน้าตาของ oAW template มีหน้าตาแบบนี้
    ให้ความรู้สึกเหมือน XSL
    «DEFINE Root FOR data::DataModel»
    «EXPAND Entity FOREACH entity»
    «ENDDEFINE»

    «DEFINE Entity FOR data::Entity»
    «FILE name + ".java"»
    public class «name» {
    «EXPAND Attribute FOREACH attribute»
    }
    «ENDFILE»
    «ENDDEFINE»

    «DEFINE Attribute FOR data::Attribute»
    private «type» «name»;
    «ENDDEFINE»


แค่นี้ก็ได้แนวทางการ Generate code แบบง่ายแล้ว
(รายละเอียดมันเยอะกว่านี้
มีทั้งเรื่อง การ validate model โดยใช้ OCL,
การสร้าง helper code ที่ template สามารถใช้ช่วยในการ generate source code)

อืมม์ นี่แค่เริ่มต้น
เนื่องจากเห็นมองเห็นภาพรวมแล้ว
ทำให้รู้สึกสนุก และอยากหัดใช้ project ในตระกูล Eclipse MDD มากขึ้น
ตัว xText ที่เป็น Textual DSL ก็น่าใช้
พวกรุ่นหลังของ GMF ก็ออกมาง่ายมากขึ้นทุกที
ทำให้เราสร้าง Graphice Editor ได้ง่ายขึ้นเรื่อยๆ

์์Note: ต้องขอบคุณงาน NJUG มาก
เพราะถ้าไม่ได้ไปฟัง Robert พูด
ตอนที่เห็น oAW ก็คงไม่ได้ปิ๊งขนาดนี้หรอก

Related link from Roti

Monday, September 17, 2007

น้องใหม่ กับ CruiseControl

เห็น Roof เปรยว่าอยากจะใช้ cruisecontrol
วันนี้ก็เลย capture ผลการใช้ในเดือนที่ผ่านมา มาให้ดู



เนื่องจากเดือนที่ผ่านมามีน้องใหม่เข้าร่วมโปรเจค 3 คน
เป็นสภาวะที่เหมาะมากสำหรับการใช้ cruise
เพราะน้องใหม่ส่วนใหญ่จะไม่ค่อยสนใจเรื่อง build รวม (แค่หัดใช้ framework ก็แย่แล้ว)
การใช้ cruise ก็เป็นวิธีที่ดีในการช่วยสร้าง discipline ให้น้องๆ
วิธีการก็ง่ายมาก
  1. ติดตั้ง cruisecontrol
  2. ลง cruisecontrol monitor ที่เป็น firefox extension
  3. ระหว่างทำงาน ผมก็คอยชำเลืองดู monitor, พอมันแดงปุ๊บ ผมก็จะพูดออกมาดังๆ "build fail โว้ย" เพื่อให้ทุกคนรับรู้
  4. ดูใน svn log ว่าก่อน fail มีใคร commit เข้ามาล่าสุด
  5. ประกาศเสียงดัง "xxx คุณ commit อะไรเข้ามาน่ะ" ด้วยน้ำเสียงเหี้ยมๆ

ข้อมูลเชิงสถิติ
  1. เปอร์เซนต์ของน้องใหม่ที่ทำ build fail คิดเป็น 100%
  2. สาเหตุหลักๆมี 2 เรื่อง
    1. commit ไม่ครบ เช่น commit interface แต่ไม่ commit Implementation
    2. มีปัญหากับ Fixtures ของ Testcase, (เป็นปัญหาเพราะใช้ test database รวม, ไม่เข้าใจ concept, อีกทั้งความผิดทางฝั่งพี่ๆ ที่ยังขี้เกียจสอนน้อง install database กับสอนการใช้งาน, ซึ่งจะทำให้น้องๆมี sandbox database ของตัวเอง)
  3. ถ้าดูจาก graph, ระยะเวลาปรับนิสัย ใช้เวลาประมาณ 3 อาทิตย์

Related link from Roti

Friday, September 14, 2007

Ofbiz Screen

ถ้าใครลองเปิด source code ของ ofbiz ดู
จะเห็นว่า หน้าจอของ ofbiz ส่วนใหญ่ render โดยใช้ screen component

ลองดูตัวอย่าง definition ของ screen สักอัน
<screen name="NewInvoice">
<section>
<actions>
<set field="title" value="New Invoice"/>
<set field="titleProperty" value="PageTitleEditInvoice"/>
<entity-one entity-name="Invoice" value-name="invoice"/>
</actions>
<widgets>
<decorator-screen name="CommonInvoiceDecorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<section>
<widgets>
<label style="head1" text="${uiLabelMap.AccountingCreateNewSalesInvoice}"></label>
<include-form name="NewSalesInvoice" location="component://accounting/webapp/accounting/invoice/InvoiceForms.xml"/>
<label style="head1" text="${uiLabelMap.AccountingCreateNewPurchaseInvoice}"/>
<include-form name="NewPurchaseInvoice" location="component://accounting/webapp/accounting/invoice/InvoiceForms.xml"/>
</widgets>
</section>
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>


ลองมาดูว่า feature ของ screen นั้นมีอะไรบ้าง
เริ่มที่ child element ของ screen ก่อน กำหนดไว้ว่าต้องเป็น <section> เท่านั้น

<screen>
<section>
...
</section>
</screen>


ภายใน section สามารถมี element ได้ 4 แบบ
<screen>
<section>
<condition>...</condition>
<actions>...</actions>
<widgets>...</widgets>
<fail-widgets>...</fail-widgets>
</section>
</screen>

ตัว condition ก็คือ expression ที่จะถูก evaluate เมื่อ screen เริ่มทำงาน
ถ้าได้ผลลัพท์เป็น true ก็จะ
ทำการเรียกใช้ actions และ render output โดยใช้ block widgets
แต่ถ้าได้ผลลัพท์เป็น false ก็จะ
render ด้วย block fail-widgets แทน

Note: ตัว condition, action, และ fail-widgets ถือว่าเป็น optional element
จะมีหรือไม่มีก็ได้

condition block ส่วนใหญ่จะไว้ใช้ check พวก authorize เช่น
<condition>
<or>
<if-has-permission permission="ORDERMGR" action="_VIEW"/>
</or>
</condition>

ส่วนภายใน action block, มีคำสั่งให้ใช้อีก 9 คำสั่ง
ซึ่งขอยกรายละเอียดไปพูดใน post หน้า

ภายใน widgets หรือ fail-widgets เราสามารถมี element ได้ดังนี้
  • section
    Note: จะเห็นว่าใน widgets ก็สามารถมี section ซ้อนอยู่ข้างในได้อีก
  • container
    container ก็คือ wrapper ที่ไว้จัดกลุ่ม widget
    การทำงานภายในของมัน ก็คือเวลามัน render html มันจะ render
    <div> block คร่อม widget ที่อยู่ข้างในมัน
  • include-screen
    อันนี้ตรงไปตรงมา ก็คือ include screen อื่นๆเข้ามา
  • decorator-screen, decorator-section-include
    อันนี้ถือเป็นหัวใจของการใช้ screen
    ถ้าเราสังเกตดูหน้าจอของ ofbiz เวลาใช้งาน
    จะเห็นว่าเวลาเราเลือก action ต่างๆ หน้าจอส่วนใหญ่จะไม่เปลี่ยนแปลง
    ส่วนที่เปลี่ยน จะเป็นแค่ region เล็กๆเท่านั้น
    ofbiz ก็เลยนำ decorator pattern มาใช้สำหรับ render code ที่ซ้ำๆกัน
    วิธีใช้ก็คือ
    <screen>
    <section>
    <widgets>
    <decorator-screen name="CommonFixedAssetDecorator" location="${parameters.mainDecoratorLocation}">
    <decorator-section name="body">
    ... widget go here.
    </decorator-section>
    </decorator-screen>
    </widgets>
    </section>
    </screen>

    <screen name="CommonFixedAssetDecorator">
    <section>
    <widgets>
    <decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
    <decorator-section name="body">
    ....
    <decorator-section-include name="body"/>
    </decorator-section>
    </decorator-screen>
    </widgets>
    </section>
    </screen>

    Note: ใน ofbiz เรามักจะเห็น decorator ซ้อนไปซ้อนมาจนน่าปวดหัว
  • label
    render string ธรรมดา
    ซึ่งกรณีที่ render ออก html ก็จะมี ครอบให้ด้วย
  • include-form, include-menu, include-tree, content, sub-content, link, image, iterate-section
    อันนี้เป็นเรื่องใหญ่อีกเรื่อง ที่จะยังไม่พูดถึง
  • platform-specific
    อันนี้พบบ่อยมาก
    วิธีใช้ก็ fix ตายตัว นั่นคือใช้เรียก html-template มาทำงาน
    โดย support เฉพาะ Freemarker เท่านั้น
    <platform-specific>
    <html>
    <html-template location="component://accounting/webapp/accounting/invoice/sendPerEmail.ftl"/>
    </html>
    </platform-specific>

Related link from Roti

Thursday, September 13, 2007

CSP กับ คำถามของไอน์สไตน์

ช่วงนี้หันกลับมาเรียนรู้ Constraint programming ใหม่
กะจะเอาจริงมากขึ้น
คราวก่อนพลาดไปตรงที่
ดันไปใช้ constraint programming library บน Oz
ทำให้เปิดศึกสองด้าน เพราะต้องเรียนรู้ Oz ไปด้วย

มาคราวนี้เลยเลือกใช้ choco
ซึ่ง base อยู่บน java
(จริงๆอยากลองใช้ chr บน prolog นะ
แต่จะเข้าข่ายเดิมอีก ก็คือ ไปไม่ถึงไหน)

ลองโจทย์แรกก่อน Zebra Puzzle
(หรือที่นิยมเรียก "คำถามของไอน์สไตน์")

modeling ของเราก็คือ
มีบ้าน 5 หลัง
เรากำหนดให้แต่ละหลังแทนด้วยตัวเลข 1 ถึง 5

จากนั้นเราก็จะกำหนด variable มาเพื่อระบุว่า
คุณสมบัติที่ variable นั้น represent นั้น, มันตรงกับบ้านหลังใด เช่น
var colors[GREEN] = 3 (บ้านหลังที่ 3 เป็นสีเขียว)
แน่นอนว่า ในเบื้องต้น เราไม่รู้หรอกว่าบ้านสีเขียวจะตรงกับหลังไหน
ดังนั้น เราจะกำหนดแบบนี้แทน
var colors[GREEN] = 1..5
นั่นคือ บ้านที่เป็นสีเขียวเป็นไปได้ตั้งแต่หลังที่ 1 ถึงหลังที่ 5

ว่าแล้วก็ลองเขียนโปรแกรมดู
เริ่มจากสร้าง problem ขึ้นมาก่อน
Problem pb = new Problem();

จากนั้นก็สร้างตัวแปร Domain variable
IntDomainVar[] colors = pb.makeEnumIntVarArray("colors", 5, 1, 5);
IntDomainVar[] nationals = pb.makeEnumIntVarArray("nationals", 5, 1, 5);
IntDomainVar[] pets = pb.makeEnumIntVarArray("pets", 5, 1, 5);
IntDomainVar[] professions = pb.makeEnumIntVarArray("professions", 5, 1, 5);
IntDomainVar[] drinks = pb.makeEnumIntVarArray("drinks", 5, 1, 5);

parameter ตัวที่ 2 คือ ขนาด ของ array ที่ต้องการสร้าง
ส่วนความหมายของ parameter 1, 5 ที่อยู่ด้านหลัง ก็คือ range ของ domain value
ซึ่งหมายความว่ามี ค่าที่เป็นไปได้คือ 1,2,3,4 หรือ 5

เมื่อมี variable พร้อมแล้ว
ก็มาถึงขั้นที่ต้องกำหนด constraint
เริ่มด้วย constraint แรก
"The Englishman lives in the red house."
เขียนได้ดังนี้
pb.post(pb.eq(nationals[ENGLISH], colors[RED]));

pb.post คือคำสั่งที่สั่ง post constraint เข้าสู่ problem space.

พวกเงื่อนไขหลักๆยังง่ายอยู่ ก็ลอกๆตามไป
// The Spaniard has a dog:
pb.post(pb.eq(nationals[SPANISH], pets[DOG]));
// The Japanese is a painter
pb.post(pb.eq(nationals[JAPANESE], professions[PAINTER]));
// The Italian drinks tea
pb.post(pb.eq(nationals[ITALIAN], drinks[TEA]));
// The Norwegian lives in the first house on the left
pb.post(pb.eq(nationals[NORVEGIAN], 1));
// The owner of the green house drinks coffee:
pb.post(pb.eq(colors[GREEN], drinks[COFFEE]));
// The green house is on the right of the white house
pb.post(pb.eq(colors[GREEN], pb.plus(colors[WHITE], 1)));
// The sculptor breeds snails
pb.post(pb.eq(professions[SCULPTOR], pets[SNAILS]));
// The diplomat lives in the yellow house
pb.post(pb.eq(professions[DIPLOMAT], colors[YELLOW]));
// They drink milk in the middle house
pb.post(pb.eq(drinks[MILK], 3));
// The violinist drinks fruit juice
pb.post(pb.eq(professions[VIOLINIST], drinks[JUICE]));

ตัวที่เริ่มยาก (ยากตรงจะใช้ syntax ของ choco อันไหนดี)
ก็คือ "The Norwegian lives next door to the blue house"
ความหมายก็คือ |Norwegian - Blue| = 1
แต่ absolute ใน choco มันรับ parameter แปลกๆ ก็เลยหันไปใช้พวก Or แทน
ได้ออกมาอย่างนี้
// The Norwegian lives next door to the blue house
pb.post(
pb.makeDisjunction(new Constraint[] {
pb.eq(pb.minus(nationals[NORVEGIAN], colors[BLUE]), 1),
pb.eq(pb.minus(nationals[NORVEGIAN], colors[BLUE]), -1)
}));
// The fox is in the house next to the doctor’s
pb.post(
pb.makeDisjunction(new Constraint[] {
pb.eq(pb.minus(pets[FOX], professions[DOCTOR]), 1),
pb.eq(pb.minus(pets[FOX], professions[DOCTOR]), -1)
}));

// The horse is in the house next to the diplomat’s:
pb.post(
pb.makeDisjunction(new Constraint[] {
pb.eq(pb.minus(pets[HORSE], professions[DIPLOMAT]), 1),
pb.eq(pb.minus(pets[HORSE], professions[DIPLOMAT]), -1)
}));


สุดท้ายก็กฎพื้นฐานที่ว่า บ้านของแต่ละคุณสมบัติ ต้องไม่ซ้ำกัน
เช่นบ้านแต่ละหลังต้องสีไม่ซ้ำกับบ้านหลังอื่นๆ
pb.post(pb.allDifferent(colors));
pb.post(pb.allDifferent(nationals));
pb.post(pb.allDifferent(pets));
pb.post(pb.allDifferent(professions));
pb.post(pb.allDifferent(drinks));


เมื่อ define constraint เสร็จ ก็สั่งให้มันค้นหาคำตอบให้เราได้
โดยเราสามารถใช้ method getVal เพื่อดึงค่าคำตอบจาก Domain variable ได้เลย
pb.solve();
pb.getSolver().getSearchSolver().restoreBestSolution();

// who drink waters
for (int i = 0; i < 5; i++) {
if (nationals[i].getVal() == drinks[WATER].getVal()) { // found
System.out.println(map.get(i) + " drink waters");
}
}
// who has zebra
for (int i = 0; i < 5; i++) {
if (nationals[i].getVal() == pets[ZEBRA].getVal()) {
System.out.println(map.get(i) + " has zebra");
}
}


Note: ใช้ java เขียนพวก declarative นี่ช่างเยิ่นเย้อจริงๆ
ให้สั้นลงอีกหน่อย ก็คงต้องใช้ groovy (ตัวอย่าง Groovy ที่เรียกใช้ choco)

Related link from Roti

Friday, September 07, 2007

before_filter

ช่วงนี้อ่านที่พี่ Conductor กับ อาจารย์ธวัชชัยกำลัง Tune performance
site gotoknow.org ที่เขียนด้วย Rails
ในชื่อ opensource project ว่า knowleageVolution หรือย่อๆว่า kv

ผมก็เลยฉวยโอกาส checkout เจ้า kv ออกมาเรียนรู้เสียเลย
สำหรับผมการอ่าน code ของคนอื่น ถือเป็นการเรียนรู้ที่ดีมาก
เพราะจะทำให้เราเห็นมุมมองอะไรบางอย่างที่เราไม่เคยเห็น
(ยิ่งของอาจารย์ธวัชชัย ยิ่งต้องควรอ่าน)

พอเปิด code ของ Controller มา
สิ่งแรกที่สะดุดสายตา ก็คือ การ reuse code โดยใช้ filter feature ของ rails

class BlogController < ApplicationController

before_filter :authenticate_user, :only => [:create, :post, :edit, :theme, :enable, :disable, :blog_post_edit, :blog_post_delete, :blog_post_delete_comment]

before_filter :get_blog_and_owner, :except => [:index, :tag_index, :tag, :create, :enable, :rss20, :rss20_redirect]
before_filter :authenticate_owner_as_user, :only => [:post, :edit, :theme, :disable, :blog_post_edit, :blog_post_delete, :blog_post_delete_comment]

before_filter :get_post, :only => [:blog_post_view, :blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]
before_filter :make_feed, :except => [:index, :create, :tag_index, :tag, :enable, :rss20, :rss20_redirect, :blog_post_newer, :blog_post_older, :recently_commented_posts]

before_filter :set_icon
before_filter :set_theme, :except => [:index, :tag_index, :tag, :create, :edit, :theme, :enable, :rss20, :rss20_redirect, :recently_commented_posts]

before_filter :process_page, :only => [:recently_commented_posts]

before_filter :set_blog_context_menu, :only => [:blog, :toc, :blog_post_view]

after_filter :expire_blog_rss, :only => [:post, :blog_post_edit, :blog_post_delete]
after_filter :expire_planet_rss, :only => [:post, :blog_post_edit, :blog_post_delete]

after_filter :expire_blogs_fragments, :only => [:create, :edit, :enable, :disable]
after_filter :expire_blog_fragments, :only => :edit
after_filter :expire_post_fragments, :only => [:blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]

after_filter :expire_home_index_posts, :only => [:edit, :enable, :disable, :post, :blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]

after_filter :expire_post_related_fragments, :only => [:post, :blog_post_edit, :blog_post_delete]

caches_page :blog_post_rss20

def index
@subject = _("Recent Blogs")
@pages, @blogs = paginate(:blogs, :conditions => "disabled = false", :order => "created_at DESC")
@feed = [_("Recent Blogs"), url_for(blog_rss20_url)]
render :template => "blog/blog_list"
end

...

ใน rails ทุกๆ action ที่ request เข้ามา เราสามารถ attach filter เข้ากับ
request นั้นได้ โดยมี filter ให้เลือก 3 แบบคือ before, after, around
(เหมือน AOP ไหม)
ปกติที่ใช้กัน ก็มักจะเอาไปทำพวก logging, authenticate, authorize
แต่ของ kv ไปไกลกว่านั้น ก็คือเอามา reuse logic ส่วนที่ใช้ร่วมกันด้วย

ยกตัวอย่าง สมมติเราขอ request ไปที่ http://gotoknow.org/blog/periphery/125115
ตัว route.rb จะ map url request ของเราเข้ากับ blog_controller.rb และเรียกใช้ method ที่ชื่อ blog_post_view

# route.rb
map.blog_post_view "blog/:address/:id", :controller => "blog", :action => "blog_post_view", :id => /\d+/

# blog_controller.rb
def blog_post_view
@post.increase_hit(session.session_id, request.remote_ip)
process_comment_page @post
@context_menu << :post
end

จาก code จะเห็นว่า logic ใน blog_post_view มันดูน้อยเสียเหลือเกิน จนไม่น่าจะทำงานอะไรได้
ที่เป็นเช่นนี้เพราะ logic ส่วนใหญ่ถูกดึงออกไปอยู่ใน fiter หมดแล้ว
ถ้าเราไปไล่ดู code ในส่วน filter เราก็จะเห็นว่า ก่อนที่จะเข้า blog_post_view
มันจะมี sequence ดังนี้

+ get_blog_and_owner
+ get_post
+ make_feed
+ set_icon
+ set_theme
+ set_blog_context_menu

=> blog_post_view

จุดอ่อนของวิธีนี้ ก็คือตอนที่ไล่ code ครั้งแรกมันอาจจะทำให้สับสนว่า logic ต่างๆมันเป็นอย่างไรบ้าง
เพราะวิธีการ declare filter มันมีทั้งแบบที่บอกว่า ฉันครอบคลุมเฉพาะอันนี้นะ (only)
กับ ฉันไม่ครอบคลุมเรื่องนี้นะ (exclude) ทำให้ต้องสลับ logic ไปมา เวลาไล่สายตา
(โชคดีอย่างหนึ่งที่ rails มันสามารถรวม code ของ filter ไว้ใน file เดียวกันได้
ทำให้การไล่ code ไม่ยากจนเกินไป)
แต่หลังจากคุ้นกับ code แล้ว การแยก logic ใน pattern แบบนี้
ก็ไม่ได้ทำให้เกิดอุปสรรคอะไร

ข้อสังเกต: rails implement feature Filter ในแบบ recursive
ดังนั้นเวลาเกิด error ขึ้นมา, stack trace ก็เลยยาวเหยียดแบบนี้

...
.//app/controllers/application.rb:275:in `add_object_comment'
.//app/controllers/blog_controller.rb:275:in `blog_post_comment'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
...

Related link from Roti

Wednesday, September 05, 2007

gem ผิดตัว

วันนี้ checkout project ที่น้องๆทำ ออกมาทดสอบ
เริ่มด้วยการสร้าง table ด้วยคำสั่ง rake migrate
ได้ error กลับมา

Cannot find gem for Rails ~>1.2.3.0:
Install the missing gem with 'gem install -v=1.2.3 rails', or
change environment.rb to define RAILS_GEM_VERSION with your desired version.

จำไม่ได้แล้ว บนเครื่องเป็น rails version อะไร
จัดแจง gem install ตามคำแนะนำทันที

(ผ่านไป 8 นาที, network บ้านเราทำไมมันช้านัก)

หลังจาก message install successfully แสดงขึ้นมา
ก็จัดแจงเรียก rake migrate อีกที
แต่ก็ต้องผิดหวัง เมื่อพบ error ตัวเดิมขึ้นมาอีก

เรียกใช้คำสั่ง rails --version
ก็พบว่าเป็น version 1.2.3 แล้ว

งม งม งม ...
(ผ่านไปอีก 15 นาที)

ลองใช้คำสั่ง gem env
ได้ข้อมูลมาดังนี้

RubyGems Environment:
- VERSION: 0.9.4 (0.9.4)
- INSTALLATION DIRECTORY: /media/sda6/pphetra/app/jruby-1.0/lib/ruby/gems/1.8
- GEM PATH:
- /media/sda6/pphetra/app/jruby-1.0/lib/ruby/gems/1.8
- REMOTE SOURCES:
- http://gems.rubyforge.org

ฮ่วย เล่น jruby แล้วลืม set path กลับ

Related link from Roti

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

Friday, August 24, 2007

ทดลองเรียกใช้ Ofbiz service ผ่าน RMI

วันนี้ทดลองเรียกใช้ service บน Ofbiz ดู
หน้าตาของ client code เป็นดังนี้
String endPoint = "rmi://127.0.0.1:1099/RMIDispatcher";
RemoteDispatcher dispatcher = (RemoteDispatcher) Naming.lookup(endPoint);

Map ret = dispatcher.runSync("createPerson", UtilMisc.toMap("firstName", "polawat", "lastName", "phetra"));
String id = (String) ret.get("personId");


code ดูตรงไปตรงมาดี แต่ก็มีปัญหาจำนวนหนึ่ง

1. ผมสร้างโปรแกรม client ใน Eclipse
เนื่องจาก code มันอ้างถึง class ใน Ofbiz ด้วย
ดังนั้น ผมก็เลย add Ofbiz project เข้ามาเป็น dependency project ใน build path
ปัญหา ก็คือเวลา run ก็จะเกิด exception หน้าตาแบบนี้
Caused by: java.io.InvalidClassException: org.ofbiz.service.rmi.socket.ssl.SSLClientSocketFactory; 
local class incompatible: stream classdesc serialVersionUID = -6771263703492769955,
local class serialVersionUID = 266831034753376983
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:562)

สาเหตุเกิดจาก eclipse class ที่ server ของ ofbiz ใช้ (compile จาก ant)
กับ class ที่ eclipse compile มันเป็นคนละตัวกัน
ทางแก้ไขก็คือ ต้องใช้ class ตัวเดียวกัน โดยการ config build path ของ eclipse project ให้ถูกต้อง
ประเด็นถัดมา ก็คือ
แล้วเราจะ add jar ของ ofbiz ตัวไหน เข้าไปใน build path บ้าง
ตรงนี้ผมใช้วิธีค่อยๆทดลองใส่เข้าไปทีละตัว
พอมัน error ว่า classNotFound ตรงไหน ก็ใส่ตัวนั้นเพ่ิมเข้าไป
ได้ข้อสรุปว่า ต้องใช้ jar และ classpath ดังนี้
  • ofbiz-base.jar
  • ofbiz-service.jar
  • javolution-4.2.8.jar
  • jdbm-1.0.jar
  • log4j.jar
  • (classpath) ofbiz/framework/base/config


2. ปัญหาถัดไป ก็คือเวลา run แล้วมันเกิด exception นี้
Caused by: java.security.cert.CertificateException: No trusted certificate found
at org.ofbiz.base.util.MultiTrustManager.checkServerTrusted(MultiTrustManager.java:70)
at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:967)

หลังจากปล้ำกับ exception นี้ 3 ชั่วโมง ก็ได้ข้อสรุปดังนี้
เนื่องจาก RMI ของ ofbiz ใช้วิธีขี่บน SSL
เมื่อเปิด ssl connection, server จะส่ง certificate มาให้ client
ปัญหาที่เกิดก็คือ client จะต้องตรวจสอบก่อนว่า
มันควรจะเชื่อใจว่า server ตัวที่มันต่อ ใช้ตัวจริงหรือไม่
โดยมันจะทำการตรวจ certificate authority ที่ sign ให้ SSL server certificate
ว่าเชื่อใจได้หรือไม่ โดยการตรวจว่า CA certificate นั้นมีอยู่ภายใน keystore ของ current runtime หรือไม่
(ปกติ ก็คือ file jre/lib/security/cacerts)

วิธีแก้ไข ก็คือ เราต้องหา public key ของ CA ที่ sign ให้ SSL server cert. ให้เจอ
แล้วก็นำมา import เข้า jre/lib/security/cacerts ของเรา
ปัญหา ก็คือ จะหาจากไหน

วิธีแรกสุด ก็คือ ต้องรู้ก่อนว่า CA certificate มีหน้าตาเป็นอย่างไร
อันนี้ ทำได้โดย กำหนด property นี้ลงไป ตอน run client code ดังนี้
-Djavax.net.debug=ssl
ซึ่ง information ที่ได้มา ก็จะบอกว่า issuer เป็นใคร
  Key:  Sun RSA public key, 2048 bits
modulus: 17344648192569884299786164434338372557687257978285180720285392205708340271654619651329483132682296140227663296442343040152431948217141461304574537723511105703477881281981923049874873997527674061132974306266530070170417234226257753990168554412489871566243809200783575680539880294393348381403494415570636578448776926614739538191796509060477513894871026620210065065716045117073870737655887248726060382348138811502024743774034713421310777435989153209885185191144967681642453965102277302605991311051345165434979041324933404867053275745104875913849033117376130950416551314398706806603259933232713428792347281547375515443731
public exponent: 65537
Validity: [From: Fri May 04 03:36:54 ICT 2007,
To: Mon May 01 03:36:54 ICT 2017]
Issuer: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
SerialNumber: [ 463a47e6]

เยี่ยมมาก Unknown ล้วนๆ

ขั้นถัดไป ก็คือมองหา certificate ที่หน้าตาแบบนี้ ใน directory ของ ofbiz
ลองใช้คำสั่ง find . -name '*.pem' -print
ก็ได้ผลลัพท์ดังนี้
@pann[~/dev/ofbiz]$ find . -name '*.pem' -print
./framework/base/cert/certreq.pem
./framework/base/cert/demoCA/cacert.pem
./framework/base/cert/demoCA/newcerts/01.pem
./framework/base/cert/demoCA/newcerts/02.pem
./framework/base/cert/demoCA/private/cakey.pem
./framework/base/cert/newcert.pem

ตัวที่ชื่อดูสื่อสุดก็คือ cacert.pem
ลองใช้คำสั่ง keytool -printcert -file cacert.pem เพื่อดู information
ก็พบว่า ไม่ใช่ file นี้
ลองดูที่เหลืออีก 5 ตัว ก็ไม่ใช่

สุดท้ายลองแกะ code ของ ofbiz ดู
ก็พบเงื่อนงำว่า ofbiz มี keystore ของตัวเอง
โดยตั้งชื่อ file ให้มีนามสกุล .jks
@pann[~/dev/ofbiz]$ find . -name '*.jks' -print
./framework/base/config/ofbizrmi.jks
./framework/base/config/ofbizssl.jks
./framework/service/config/rmitrust.jks

อยากรู้ว่าเป็นตัวไหน ก็ต้อง browse ดู
(จำได้ว่า ถ้าเป็น JDK ของ IBM มันจะมี GUI tool มาให้ด้วย
แต่วันนี้คิดว่าจะฝึกใช้ command-line ก็เลยไม่ได้ลองค้นดู)
@pann[~/dev/ofbiz/framework/service/config]$ keytool -list -v -keystore rmitrust.jks
Enter keystore password:

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

Alias name: mykey
Creation date: May 4, 2007
Entry type: trustedCertEntry

Owner: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
Issuer: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
Serial number: 463a47e6
Valid from: Fri May 04 03:36:54 ICT 2007 until: Mon May 01 03:36:54 ICT 2017
Certificate fingerprints:
MD5: 86:B1:D1:09:74:36:66:C7:30:18:7B:16:FA:C2:C2:9E
SHA1: 90:42:E8:91:BD:4E:88:59:E4:D7:F3:10:12:8F:57:3E:81:87:96:A2
Signature algorithm name: MD5withRSA
Version: 1


จากการเปรียบเทียบดู ก็พบว่า ofgizrmi.jks กับ rmitrust.jks
เก็บ cert ตัวเดียวกัน และตรงกับที่มันฟ้องใน debug console ของเรา

ขั้นถัดมาก็คือ export cert ออกจาก keystore
โดยใช้คำสั่ง keytool -exportcert -keystore ofbizrmi.jks -file pok.pem -alias rmissl
นำ file ที่ได้ไป import เข้า jre/lib/security/cacerts โดยใช้คำสั่ง
keytool -import -keystore ./cacerts -file pok.pem -alias ofbiz_rmissl

Note: จริงเราควรใช้ keystore ของเราเอง แทนที่จะใช้ global keystore
โดยกำหนดใน system property ดังนี้
-Djavax.net.ssl.trustStore=mySrvKeystore 
-Djavax.net.ssl.trustStorePassword=123456


Note: default password ของ keystore ของ ofbiz และ java คือ "changeit"

จบ ปิดคดี

Related link from Roti