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

Tuesday, August 21, 2007

ขั้นตอนการ start ของ Ofbiz

ลองแกะขั้นตอนการ start ของ Ofbiz ดู ก็พบขั้นตอนที่น่าสนใจดังนี้

1. เหมือนกับ framework ใหญ่ๆทั่วๆไป นั่นคือ
ถ้าต้อง start อะไรที่ซับซ้อนนัก ก็ควรมี bootstrapping code
โดย class ที่เป็นจุดเริ่มต้นทำงานของ Ofbiz ก็คือ org.ofbiz.base.start.Start.java
เมื่อ initialize ค่าเบื้องต้นต่างๆหมดแล้ว (เช่น log directory, base class path)
ก็จะทำการ load configuration ตาม command parameter ที่ user สั่งผ่าน command-line มา
เช่น ผมสั่ง
java -jar ofbiz.jar -pos
มันก็จะไปใช้ configuration file ที่ชื่อ /framework/base/src/start/org/ofbiz/base/start/pos.properties
ถ้าสั่ง
java -jar ofbiz.jar
มันก็จะใช้ default configuration file ซึ่งก็คือ start.properties

element ที่สำคัยภายใน properties file ข้างบน ก็คือ ofbiz.start.loader* (* คือเลขลำดับในการ load)
ตัว loader ที่สำคัญของ Ofbiz ก็คือ org.ofbiz.base.container.ContainerLoader
ซึ่งเมื่อมัน load ขึ้นมา มันจะใช้ค่า ofbiz.container.config ที่ระบุใน configuration file
ในการ load container ขึ้นมาทำงาน

2. ofbiz มองว่า การ start ครั้งหนึ่งๆ เราสามารถกำหนด container ที่เราต้องการได้หลายตัว
โดยต้องสร้าง xml file ที่ระบุ container ที่ต้องการ
และระบุชื่อ file ไว้ใน configuration ผ่าน property ที่ชื่อ ofbiz.container.config

อย่างในกรณีที่เราจะ start โปรแกรม point of sell
ค่า ofbiz.container.config ก็จะกำหนดแบบนี้
ofbiz.container.config=framework/base/config/pos-containers.xml

ofbiz ก็จะใช้ file pos-containers.xml ในการเลือก start container ที่ต้องการ

3. Container ที่สำคัญๆ และขาดไม่ได้ ของ Ofbiz ก็คือ
3.1 component-container ทำหน้าที่ load component ต่างๆ
ทั้งที่เป็น framework component และ application component
โดย default มันจะใช้ file framework/base/config/component-load.xml เป็นจุดตั้งต้นในการ load
ซึ่งภายในจะ recursive load module ของ framework, applications, specialpurpose, hot-deploy
ถัดจากตรงนี้ไป ยังมีเรื่องให้ตามอีกยาว ดังนั้นจะพักไว้แค่ตรงนี้ก่อน

3.2 classloader-container
ตรงนี้ยังแกะไม่เข้าใจ
โดย ofbiz จะใช้ classloader ที่ชื่อ CacheClassLoader.java

จากที่ตามดูขั้นตอนการ load Point of Sell
พบว่า ofbiz มัน load ทุก component, ทุก applications
ดูเหมือนจะ load เยอะกว่าที่เราต้องการ

ประเด็นที่น่าสนใจ กรณีจะทำเป็น Eclipse RCP
1. จะ pack มันเป็น bundle(plugin) อย่างไร
การ pack เป็น bundle จะส่งผลต่อ code แค่ไหน
ใช้ wrapping อย่างเดียว เพียงพอหรือไม่
ปัญหาที่ตามมา ก็คือเรื่อง class loader
เนื่องจาก Eclipse มี model class loader ที่ค่อนข้าง strict

2. ถ้าไม่ใช้ Eclipse RCP แต่เปลี่ยนไปใช้แค่ SWT+JFace
น่าจะช่วยให้หลีกเลี่ยงปัญหาก้อนใหญ่ไปได้

Related link from Roti

Monday, August 20, 2007

ลูกหมู 3 ตัว

เมื่อวานแวะไปซื้อหนังสือนิทานที่จตุจักรให้คุณลูกชาย
(มันมีหนังสือนิทานมือสอง ที่เขาเหมาตู้ container มาจากต่างประเทศ)
เลือกได้มาจำนวนหนึ่ง ในนั้นมีเรื่องลูกหมูสามตัวด้วย

กลับมาบ้าน ก็นั่งเล่าให้คุณลูกฟัง
เรื่องหมูสามตัว ที่ทำบ้านด้วย ฟาง,ไม้,อิฐ
เนื้อเรื่องตอนแรกๆ ก็เหมือนที่เคยอ่านตอนเด็กๆ
แต่ตอนจบนี่แปลกไปจากที่เคยจำได้
มันจบที่ว่า
"ลูกหมูได้กินเนื้อหมาป่าเป็นอาหารเย็น"
เล่าจบ ก็ร้อง "เฮ้ย" ด้วยความประหลาดใจ
หันไปถามแฟน กับหลานๆที่แวะมาเยี่ยมบ้าน
ก็ไม่เคยมีใครได้ยินตอนจบแบบนี้

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

Note: แก่แล้ว เริ่มมีย้อนอดีต

Related link from Roti

Saturday, August 18, 2007

Unit Testing สำหรับ DAO

ปกติเวลาเขียน unit testing
ผมมักจะมีปัญหากับ test ในส่วนของ dao, service layer อยู่มาก
เนื่องจากมันต้องยุ่งเกี่ยวกับ Database
ซึ่งหมายถึงต้องมี Test Data เตรียมไว้ให้เรียบร้อย
ที่แย่ก็คือ พอ testing ที่ไปยุ่งกับ update, delete
มันก็จะทำให้ state ของ data ใน database เราเปลี่ยนไป

ดังนี้ปกติที่ผมทำ ก็คือ
เวลา test ใน service layer ก็ใช้พวก mock กับ stub มาช่วย
จะได้ไม่ต้องยุ่งกับ database

ส่วนใน dao layer ก็เอาพวก DBUnit มาใช้
ซึ่งก็ช่วยได้พอสมควร
แต่ก็รู้สึกว่าการใช้ DBUnit ก็ดูเป็นการทรมาน database ไปหน่อย
(delete, insert ทั้งหมด ทุกๆ test method)
หลังๆ เวลา test ก็เปลี่ยนมาใช้พวก in memory database แทน
ซึ่งผลพลอยได้ ก็คือประหยัดเวลา test ลงไปด้วย

แต่พอเป็นโปรเจคใหญ่ๆที่มี table สัก 200 table
มี data test เยอะพอสมควร
การใช้ DBUnit ก็ดูจะไม่ไหว

สุดท้าย ก็ไปลงเอยที่ AbstractTransactionalDataSourceSpringContextTests
ที่ spring เตรียมไว้ให้
โดยหลักการ ก็คือ spring มันจะเตรียม transaction ไว้ให้เราก่อนเรียก method test
และพอจบ method, spring ก็จะสั่ง rollback ให้
ซึ่งเป็นแนวคิดที่เรียบง่าย แต่ได้ผลดีมาก

แต่ก็มีปัญหาอยู่บ้าง ในกรณีที่เราใช้ Hibernate
hibernate มันจะ cache sql ไว้ (รอให้จบ transaction ก่อนจึงจะทำงาน)
ทำให้เกิดปัญหาว่า assert statement ของเรา
ทำงานไม่ถูกต้อง
ทางแก้ก็คือ เราต้องสั่งให้ hibernate flush sql statement
ก่อนที่จะทำการ assert

ปัญหาอีกอย่างที่เจอ ก็คือ ผมใช้ TestNG อยู่
แต่เจ้าตัว AbstractTransactionalDataSourceSpringContextTests
มัน base อยู่บน JUnit
ก็เลยต้องมีการดัดแปลงนิดๆหน่อยๆ
แต่ผลที่ได้ก็เจ๋งไปอีกแบบคือเลือก run ได้ทั้ง Junit และ TestNG
(พึ่งรู้ว่า เราใช้คำสั่ง assert* ของ Junit มา run ใน TestNG ได้ด้วย)

หน้าตาของ base class ของ testcase ผมก็จะเป็นแบบนี้
public class AbstractDaoTest extends
AbstractTransactionalDataSourceSpringContextTests {

protected SessionFactory sessionFactory;
protected HibernateTemplate hibernateTemplate;

public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
hibernateTemplate = new HibernateTemplate(this.sessionFactory);
}

@Override
protected String[] getConfigLocations() {
return new String[] { "classpath:test-dao.xml" };
}

@Configuration(beforeTestMethod = true, alwaysRun = true)
public void tNGsetUp() throws Exception {
super.setUp();
}

@Configuration(afterTestMethod = true, alwaysRun = true)
public void tNGtearDown() throws Exception {
super.tearDown();
}

protected void flush() {
hibernateTemplate.flush();
}
}


ตัวอย่าง test case
public class TestUserDao extends AbstractDaoTest {

UserDao dao;

@Test
public void testCreateRole() {
assertFalse(dao.getFetchRoles("u190240").isUserInRole("testing"));
dao.addRole("u190240", "testing");

flush();

assertTrue(dao.getFetchRoles("u190240").isUserInRole("testing"));
}

@Test
public void testFindAll() {
List<User> list = dao.findAll();
int cnt = jdbcTemplate.queryForInt("select count(*) from b_user_name");
assertEquals(cnt, list.size());
}

public void setDao(UserDao dao) {
this.dao = dao;
}

}

ข้อดีอีกอย่างของ AbstractTransactionalDataSourceSpringContextTests
ก็คือมัน inject dao เข้ามาให้เราโดยอัตโนมัติ
ไม่ต้องมา getBean เอง

Related link from Roti

Friday, August 17, 2007

bug bug bug

หลังจากลูกชายคนที่สองครบ 3 เดือน
แม่เด็กมีแรงดูลูกทีเดียว 2 คนแล้ว
ก็เลยได้ฤกษ์กลับไปทำงาน

วันแรกที่ทำ ก็เจอ bug ไปหลายตัวทีเดียว

เริ่มที่ตัวนี้ก่อน
เกิดจากการ update Hibernate-Annotation ไปเป็น version 3.3.0.ga
ปรากฎว่าพอสร้าง SessionFactory ก็จะเกิด exception นี้ขึ้น
Caused by: java.lang.NullPointerException
at org.hibernate.cfg.annotations.CollectionBinder.buildOrderByClauseFromHql(CollectionBinder.java:851)

ค้นดูแล้ว เป็น bug ANN-617
มีสาเหตุจากพวก @OrderBy annotation ในพวก one-to-many
เจอแล้ว ก็ถอย version กลับไปที่ 3.2.1.ga

ตัวที่สองเป็น bug ของ maven cobertura plugin
เป็นกับพวกที่ใช้ windows platform
โดยตอนที่มัน instrument class
file cobertura.ser มันสร้างไว้ผิดที่
ทำให้ตอน testing มันได้ผลลัพท์เป็น 100% coverage ตลอด
แก้โดยการกำหนด version เป็น 2.0 ลงใน pom.xml
        <plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<!-- เพิ่มตรงนี้เข้าไป -->
<version>2.0</version>
...


ตัวที่สามเป็น bug ใน maven mojo plugin บน Linux
มันจะฟ้องว่า
java.io.IOException: java: not found 

เกิดจาก execute file "java" ไม่ได้อยู่ใน system path (พวก /usr/bin, /usr/local/bin)
ทำ link ให้แล้วก็หาย

นอกเรื่อง:
กลับมาถึงบ้าน ลูกชายวิ่งเข้ามากระโดดเข้ากอด
ชื่นใจจริงๆ

Related link from Roti

Wednesday, August 15, 2007

Seed Data

ผมชอบวิธี load data ใน Ofbiz ที่ใช้วิธีประกาศ
<entity-resource type="data" reader-name="seed" loader="main" location="data/AccountingTypeData.xml"/>

ส่วน file ที่เก็บ data จะมีหน้าตาแบบนี้

<GlAccount parentGlAccountId="" glAccountId="100000" accountCode="100000"
glAccountClassId="ASSET" glAccountTypeId="" glResourceTypeId="MONEY"
accountName="ASSETS" description="" postedBalance="0.0"/>

<GlAccount parentGlAccountId="100000" glAccountId="110000" accountCode="110000"
glAccountClassId="CASH_EQUIVALENT" glAccountTypeId="CURRENT_ASSET" glResourceTypeId="MONEY"
accountName="CASH" description="" postedBalance="0.0"/>

<GlAccount parentGlAccountId="110000" glAccountId="111000" accountCode="111000"
glAccountClassId="CASH_EQUIVALENT" glAccountTypeId="CURRENT_ASSET" glResourceTypeId="MONEY"
accountName="CASH IN BANK AND ON HAND" description="" postedBalance="0.0"/>


ตอนนี้กำลังเขียนโปรแกรมบัญชีด้วย Rails อยู่เหมือนกัน
ก็เลยอยากลอกวิธี setup seed data มาไว้ใช้บ้าง

เดิิมใน rails เราสามารถ load data ที่อยู่ในรูป format YML ได้เหมือนกัน
โดย rails เรียกว่า Fixtures
แต่ข้อเสียของ Fixtures ก็คือ
มันแยกเก็บเป็น 1 file ต่อ 1 table
ซึ่งสำหรับผม ในการ config ที่ cross ไป cross มา
มันมองภาพรวมยากไปนิด

กลับมาเข้าเรื่องต่อ ถ้าเราลอก feature seed data มาทำด้วย Rails
ก็จะมี constraint ในการ design อยู่ 2 เรื่องคือ
1. เราคงไม่ใช้ xml
2. ใน ofbiz, primary key มันเป็น String, ทำให้ง่ายต่อการ reference กันระหว่าง entity
แต่ใน ruby, primary key มันเป็น integer ดังนั้นต้องมี concept ที่ใช้ตั้งชื่อ record
เพื่อให้ง่ายต่อการอ้างใช้

ตัว syntax เมื่อไม่ใช้ xml ก็ลองเอา block มาใช้ดู
หน้าตาแบบนี้
setup do |s|

s.model :AcctChart do |m|
m.data :all, :code => "all", :name => "Mx Group"
end

s.model :AcctGroup do |m|
m.data :cash, :description => "Cash"
m.data :liability, :description => "Liability"
m.data :equity, :description => "Equity"
m.data :revenue, :description => "Revenue"
m.data :expense, :description => "Expense"
end

s.model :AcctEntity do |m|
m.data :mx, :code => "mx",
:party_id => s.Organization[:mx],
:acct_chart_id => s.AcctChart[:all]
end

s.model :AcctChartItem do |m|
m.data 1000,
:code => "1000",
:long_name => "cash"
m.data 1001,
:code => "1010",
:short_name => "cash on hand",
:acct_chart_id => s.AcctChart[:all],
:acct_group_id => s.AcctGroup[:cash],
:parent_id => m[1000]
m.data 1002,
:code => '1020',
:short_name => "cash in bank",
:acct_chart_id => s.AcctChart[:all],
:acct_group_id => s.AcctGroup[:cash],
:parent_id => m[1000]
end

end

จะเห็นว่า เราใช้วิธีอ้างถึง record ที่เกิดขึ้นแล้ว อยู่ 2 แบบ

:acct_chart_id => s.AcctChart[:all]

:parent_id => m[1000]

ทั้งสองวิธีใช้ hash เหมือนกัน แต่ต่างกันที่ scope ที่ระบุแค่นั้น

สำหรับการ implement, ประเด็นที่สนุกหน่อย ก็คือ
เรารู้ Model Class โดยชื่อของมัน
จากชื่อ เราจะ solve ไปเป็น class Object ได้อย่างไร
อย่างใน java เราก็จะมี Syntax พวกนี้ใช้
Class.forName("x.y.Z").newInstance()


ใน ruby ถ้าเล่นแบบลูกทุ่งสุด ก็ง่ายๆแบบนี้

eval("#{clsName}.new")


ถ้าให้สวยขึ้น ก็ต้องมองที่ concept ว่า Class ใน ruby
มันเก็บไว้เป็น constant ใน Object
ดังนั้นเราสามารถใช้คำสั่งแบบนี้ได้
Objet.const_get(clsName).new


ตัว code เต็มๆ หน้าตาแบบนี้
module DataUtil
module Setup
def setup()
setup = Setup.new()
yield setup
end

class Setup
def initialize()
@cache = {}
end

def model(name)
puts "loading model #{name}"
m = Model.new(name)
@cache[name] = m
yield m
end

def method_missing(sym)
@cache[sym]
end
end

class Model
def initialize(name)
# delete old data
Object.const_get(name).delete_all
@name = name
@cache = {}
end

def data(id, attrs)
m = (Object.const_get @name).new(attrs)
m.save
internal_id = m.id
@cache[id] = internal_id
end

def [](key)
@cache[key]
end
end
end
end


ตัว config ทำให้สวยขึ้นได้กว่านี้
เช่น เปลี่ยนไปอ้างตรงๆเลย แทนที่จะใช้ hash accessor
:acct_chart_id => :all,
:acct_group_id => :cash,

แต่เข้าข่าย 80-20 แล้ว
เมื่อได้ feature เพียงพอต่อการใช้งานแล้ว ก็จงหยุด

Related link from Roti