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

Friday, August 10, 2007

alias_method_chain

เมื่อวานนั่งดู source code ของ Rails
ก็ไปพบกับเจ้า method aliase_method_chain

หลักการของมัน ก็ทำนองเดียวกัย around advise ของพวก AOP
ก็คือสมมติเรามี
class Payment
def say
"hi"
end
end


ถ้าเราต้องการเปลี่ยนพฤติกรรมของมัน
เช่นต้องการเติม prefix ข้างหน้า
ก็สามารถ re-open class ได้ดังนี้
class Payment
def say_with_prefix
">> " + say_without_prefix
end

alias_method_chain :say, :prefix
end

จากข้างบน alise_method_chain จะทำการ
เปลี่ยนชื่อ method เดิมจาก "say" -> "say_without_prefix"
แล้วก็ปรับให้การเรียกใช้ "say" ไปผ่านเข้าทาง "say_with_prefix" แทน

ข้างบนลองเขียนให้สั้นๆ
แต่ pattern ใน rails ที่เขานิยมเขียนกัน
เขาจะไม่เขียนอย่างนี้
เขาจะเขียนโดยใช้ module เข้ามาช่วย
โดยตั้ง namespace ให้เป็นระเบียบ
ดังนั้นในส่วน re-open class ข้างบน เขาจะเขียนใหม่เป็นแบบนี้แทน
(Note: ชื่อ module ก็แล้วแต่จะตั้ง แต่มีกี่ชั้นก็ว่าไป)
module PAYMENT
module AROUND
def self.included(base)
base.class_eval do
alias_method_chain :say, :prefix
end
end

def say_with_prefix
">> " + say_without_prefix
end
end
end

และเมื่อไรก็ตามที่ต้องการเปลี่ยนพฤติกรรมของ class Payment ก็ให้สั่ง
Payment.send(:include, PAYMENT::AROUND)


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

alias_method :say_without_prefix, :say
alise_method :say, :say_with_prefix

ผมว่า aliase_method_chain ดูเข้าใจง่ายกว่านะ

Related link from Roti

Thursday, August 09, 2007

redo, undo ใน visualwork (smalltalk)

http://www.cincomsmalltalk.com/files/bobw/screencasts/Refactoring_Steroids/

พึ่งรู้ว่า refactoring ใน smalltalk มันทำในขณะที่โปรแกรม(ที่ถูก refactor) กำลังงานอยู่ได้
โดยไม่ต้อง start, stop โปรแกรมเลย
แถมยังทำ redo-undo ได้อีกด้วย (ไม่ต้อง restart โปรแกรมเช่นกัน)

เป็นสภาพแวดล้อมในการพัฒนาที่ดูดีมากเลย.

Related link from Roti

Monday, August 06, 2007

JGoodies binding.

วันนี้ลองเล่น JGoodies Binding จริงๆจังๆดูบ้าง
หน้าจอที่จะทดสอบ มีหน้าตาแบบนี้



ด้านซ้ายเป็น JList และด้านขวาเป็น edit panel

เริ่มด้วย Domain Model ก่อน
ก็คือ Person
public class Person extends Model {

private String name;
private double salary;

}

สังเกตว่า extends จาก com.jgoodies.binding.beans.Model
Note: ถ้า binding แบบง่ายๆ เราไม่ต้อง extend Model ก็ได้
แต่ถ้า feature ที่ใช้ซับซ้อนขึ้น มันบังคับให้เราต้อง extend Model.

แนวคิดหลัก ของ JGoodies (ซึ่งลอกมาจาก VisualSmalltalk อีกที)
ก็คือ ValueModel
GUI Widget จะไม่ access ข้อมูลผ่าน Domain Object ตรงๆ
แต่จะ set หรือ get ข้อมูล ผ่านทาง ValueModel

ตรงนี้อาจจะสับสนกับ Swing Model พวก TableModel, ListModel นิดหน่อย
ตรงที่ Swing model ก็มีวัตถุประสงค์ทำนองเดียวกันกับ Value Model
แต่ว่า Swing model ไม่ได้มีลักษณะเป็น universal model
ที่สามารถใช้ร่วมกันทั้งหมด ไม่ว่าจะเป็น widget ชนิดไหน แบบเดียวกับ ValueModel

ข้อดีของ Universal Model แบบ ValueModel
นอกจากจะทำให้การเขียน โปรแกรม เป็น pattern เดียวกันหมดแล้ว
ยังมีข้อดีในแง่ที่เราสามารถ นำ ValueModel มาต่อกันเป็น chain ได้
ทำให้เราสามารถทำ feature พวก Buffered, Convertor, Indirection ได้

กลับมาที่โปรแกรมเรา
เราจะออกแบบโปรแกรมของเรา โดยใช้ pattern Presentation Model (ใน VisualWork Smalltalk เรียก Aplication Model)
เริ่มด้วยการสร้าง PresentationModel ของเรา
public class ApplicationModel {
}


Person Object ของเรา จะเก็บอยู่ใน ArrayList แบบนี้
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();

}


step ถัดไปคือ เตรียม ValueModel ที่จะใช้ binding กับ JList,
ตัว ValueModel ที่เราจะนำมาใช้ ห่อ persons ของเราก็คือ SelectionInList
ซึ่งออกแบบมาเพื่อใช้กับพวก container widget เช่น JList, JTable.
feature ของมันก็คือ มันจะช่วย provide ValueModel ของ Current selection,selection index
ทำให้เรานำไป binding กับ widget อื่นๆได้อีก
(ซึ่งในกรณีของเรา ก็จะนำ current selection ไป binding กับ edit widget ที่อยู่ด้านขวา)
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();
private SelectionInList<Person> selectionInList;

public ApplicationModel() {
selectionInList = new SelectionInList<Person>(persons);
}

public SelectionInList getPersons() {
return selectionInList;
}
}


ในฝั่งของ GUI เวลาจะนำไปใช้ ก็จะทำประมาณนี้
public class MainFrame extends javax.swing.JFrame {

private ApplicationModel model = new ApplicationModel();

/** Creates new form MainFrame */
public MainFrame() {
initComponents();
Bindings.bind(jList1, model.getPersons());
}

...
}


step ถัดไป ก็คือการ bind current selection ใน JList เข้ากับ Editor Panel ที่อยู่ด้านขวา
ซึ่งในกรณีนี้ประกอบด้วย JTextField 2 ตัว สำหรับ name และ salary

เริ่มด้วยในฝั่ง ApplicationModel ก็เตรียม ValueModel ที่จะนำไป bind กับ JTextField
ตรงนี้เราสามารถทำได้หลายวิธี แต่วิธีที่เลือกใช้ก็คือ ใช้ com.jgoodies.binding.PresentationModel เข้ามาช่วย
(เพื่อที่จะได้ใช้ feature BufferedValueModel)
public class ApplicationModel {
private ArrayList<Person> persons = new ArrayList<Person>();
private SelectionInList<Person> selectionInList;
private PresentationModel<Person> person;

public ApplicationModel() {
selectionInList = new SelectionInList<Person>(persons);
person = new PresentationModel<Person>(selectionInList.getSelectionHolder());
}

public ValueModel getPersonName() {
return person.getBufferedModel("name");
}

public ValueModel getPersonSalary() {
return new DoubleToStringConvertor(person.getBufferedModel("salary"));
}

class DoubleToStringConvertor extends AbstractConverter {

DoubleToStringConvertor(ValueModel subject) {
super(subject);
}

public Object convertFromSubject(Object arg0) {
if (arg0 != null) {
return arg0.toString();
}
return null;
}

public void setValue(Object arg0) {
subject.setValue(new Double(arg0.toString()));
}

}
}

จาก code ข้างบน จะมีประเด็นที่น่าสนใจอยู่ 2 ประเด็นคือ
1. เราใช้ getBufferedModel ในการ return ค่า ValueModel ไปให้ GUI view
ซึ่งผลก็คือ การเปลี่ยนแปลงใดๆที่เกิดขึ้นที่ฝั่ง GUI View จะยังไม่มีผลต่อ Model
จนกว่าจะมีการสั่ง commit การเปลี่ยนแปลง
2. กรณีที่ Datatype ฝั่ง model ไม่ใช่ String ก็จะมีประเด็นเรื่องการ Convert Datatype
ซึ่งใช้เทคนิคการ chain ValueModel เข้าหากัน

ฝั่ง GUI View ก็สามารถนำไป binding ได้ดังนี้
Bindings.bind(jTextField1, model.getPersonName());
Bindings.bind(jTextField2, model.getPersonSalary());


ขั้นถัดไป ก็คือเตรียม Action ที่จะใช้เพื่อ commit การเปลี่ยนแปลงที่เกิดขึ้นใน BufferedValueModel

ฝั่ง ApplicationModel

public Action getApplyAction() {
return new AbstractAction("Apply") {

public void actionPerformed(ActionEvent arg0) {
person.triggerCommit();
selectionInList.fireSelectedContentsChanged();
}

};
}

ฝั่ง GUI View

jButton1.setAction(model.getApplyAction());


ปัญหาของ BufferdValueModel ก็คือ กรณีที่เกิดการแก้ขึ้นแล้ว
ยังไม่มีการ commit, ถ้า Selection เกิดเปลี่ยนไป ข้อมูลที่ bound ไว้
จะไม่ยอมเปลี่ยนตาม ดังนั้นเราต้องสั่ง cancel การแก้ไข ด้วยการเรียกใช้ method triggerFlush
selectionInList.addPropertyChangeListener(

SelectionInList.PROPERTYNAME_SELECTION,
new PropertyChangeListener() {

public void propertyChange(PropertyChangeEvent arg0) {
person.triggerFlush();
}

});


พอแค่นี้ก่อน ที่เหลือไม่ค่อยมีประเด็นแล้ว

Note.
1. แนวคิด + วิธีในการใช้งาน มีตัวเลือกให้ใช้มากเกินไป (many way to do one thing) ถ้า assign ให้เด็กใหม่เอาไปใช้ ไม่น่าจะรอด
2. กรณีที่มีการ refactor เปลี่ยนชื่อ property ของ Domain Model จะส่งผลกระทบเยอะ
เพราะ binding ใช้วิธีกำหนดชื่อของ property ในรุป String
3. รู้สึกว่า Groovy ก็มีใช้ pattern ValueModel ใน GUI ด้วยเหมือนกัน
(เจอใน gapi แต่ยังไม่เจอเอกสารที่พูดถึง)

Related link from Roti

Monday, July 30, 2007

กลไก Dependency ใน Smalltalk

ช่วงนี้กำลังสนใจเรื่อง binding ของ Java GUI อยู่
หลังจากที่ค้นคว้า JGoodies binding, Spring RCP binding แล้ว
ก็เลยรู้ว่าทั้งคู่ได้ แนวคิด มาจาก ValueModel ของ VisualWork Smalltalk
ผมก็เลยอยากรู้ว่า smalltalk ตััวจริง เขามีแนวคิดในการ implement อย่างไรบ้าง

จากการแกะ code ของ ValueModel ใน smalltalk ก็พบว่ารากฐานของ ValueModel ก็คือกลไก Dependency
ซึ่งถ้าเปรียบเทียบกับฝั่ง java แล้วก็คือพวก กลไกพวก PropertyChangeSupport, PropertyChangeEvent ใน java bean
แต่ที่ต่างกันอย่างมาก ก็คือ ในฝั่ง Java กลไกพวกนี้ ถ้าอยากได้ เราต้อง implement มันเอง
แต่ในฝั่ง Smalltalk, กลไกพวกนี้มันฝังอยู่ใน Object class เลย
ดังนั้น object ทุกตัวใน virtual machine เรียกใช้กลไกนี้ได้ทั้งหมด

เริ่มต้นที่ Model Object ที่เราต้องการให้เป็นคน fire changed event ก่อน
ใน smalltalk ถ้าเราต้องการ fire change ก็ทำได้โดยเรียกใช้ method changed
(implement อยู่ใน Object class)
price: aPrice
| oldPrice |
oldPrice := price.
price := aPrice.
self changed: #price with: oldPrice.


ส่วน class ที่สนใจจะ track change ที่เกิดขึ้น ก็ทำได้โดย
1. register ตัวเองเข้ากับ Model object ที่ต้องการ track
โดยใช้ method addDependent
initialize: aStock
aStock addDependent: self

2. override method ที่ชื่อ update ซึ่งจะถูก call จาก model object เมื่อมีการเปลี่ยนแปลง
update: aSymbol with: oldPrice from: aStock
Transcript cr; print: thisContext; tab; print: aSymbol; tab.
aSymbol == #price ifTrue:
[Transcript
nextPutAll: 'Price changed from: '; print: oldPrice;
nextPutAll: ' to '; print: aStock price].
Transcript flush.

จากวิธีข้างบน เราจะเห็นว่า เรามี if statement ที่ใช้ตรวจสอบว่า message ที่ส่งมานั้นเป็น message ที่เราสนใจหรือเปล่า
ดูแล้วในทางปฏิบัติมันไม่น่าจะ work เท่าไร เนื่องจากถ้ามี message มากๆ
if statement หรือ switch statement ก็จะยาว และทำให้ code ดูเลอะเทอะ

เพื่อให้ code เรากระชับขึ้น
เราสามารถเลือก register เฉพาะ change ที่เราสนใจได้โดยใช้ method "expressInterestIn"
โดยเปลี่ยน code ข้างบนในข้อ 1 เป็นดังนี้แทน
initialize: aStock
aStock expressInterestIn: #price
for: self
sendBack: #priceChanged:by:

จากนั้นก็เปลี่ยน implement method update ในข้อ 2
เป็นชื่อที่เรา send เป็น parameter sendBack:
priceChanged: oldPrice by: aStock
Transcript
cr; print: thisContext; tab;
nextPutAll: 'Price changed from: '; print: oldPrice;
nextPutAll: ' to '; print: aStock price; flush


ปัญหาของ Dependency ใน Smalltalk ก็เป็นเช่นเดียวกับ Java
นั่นก็คือต้องระวังเรื่อง memory leak ที่เกิดจาก Garbage collector ไม่สามารถ remove object ได้
เนื่องจากมี dependency ค้างอยู่

กรณีตัวอย่างที่ยกมาข้างบน จะมีปัญหา memory leak ได้ง่ายได้
เนื่องจากกลไกการ implement ของ dependency list มันทำที่ระดับ class variable
เขาจึงแนะนำว่า ตัว Model Class ของเรา ควรจะ extends จาก Model class จะดีกว่า
เนื่องจาก Model Class มันจะ override กลไกการใช้ class variable มาใช้ instance variable
ในตัวเองแทน ทำให้ลดโอกาสเกิด memory leak ได้

Related link from Roti

Wednesday, July 25, 2007

ก้าว(bug)แรกใน OFBiz

หลังจากได้ความรู้จากน้อง sand ที่บรรยายในงาน NJUG #3 มาแล้ว
ก็ถึงเวลาที่ต้องทดลองเสียหน่อย (โหลดมาทิ้งไว้นานแล้ว แต่ไม่ได้เริ่มสักที)

การ install และ run คงไม่ต้องพูดถึง เพราะค่อนข้างง่าย
เริ่มเล่าบรรยากาศที่ได้จากการใช้ดีกว่า
ผมเริ่มด้วยการใช้ module accounting/Chart of Accounts ก่อน
เรื่องแรกที่พยายามจะทำก็คือ การบันทึก Journal Entry
ดูจากหน้าตาที่ OFBiz ให้มาแล้ว ช่างเป็น minimum feature เสียนี่กระไร
ให้ป้อน entry ได้แค่แบบ 1 debit ต่อ 1 credit เท่านั้น



ทดลองใส่ข้อมูล ก็พบว่ามันเกิด error แดงเถือก



อ่าน message ดูแล้ว ก็พบว่ามันเป็น error ที่ระดับ database

violation of foreign key constraint 'ACCTTXENT_GLACOG' for key (121900,Company).

ตามไปดูใน model definition (applications/accounting/entitydef/entitymodel.xml)
ก็จะเห็นดังนี้
<relation type="one" fk-name="ACCTTXENT_GLACOG" rel-entity-name="GlAccountOrganization">
<key-map field-name="glAccountId"/>
<key-map field-name="organizationPartyId"/>
</relation>

จะเห็นว่ามัน link กับ entity GlAccountOrganization
ถ้าตามไปดูต่อ ก็จะเห็น definition ดังนี้
<entity entity-name="GlAccountOrganization"
package-name="org.ofbiz.accounting.ledger"
title="GL Account Organization Entity">
<field name="glAccountId" type="id-ne"></field>
<field name="organizationPartyId" type="id-ne"></field>
<field name="roleTypeId" type="id-ne"></field>
<field name="fromDate" type="date-time"></field>
<field name="thruDate" type="date-time"></field>
<field name="postedBalance" type="currency-amount"></field>
...

อ้าฮ้า มันคือ table ที่เก็บความสัมพันธ์ระหว่างหน่วยงาน กับ รหัสบัญชี
ลอง select ข้อมูลดูโดยใช้ webtool ก็พบว่า มันมีข้อมูลมาให้รายการเดียว

ดังนั้นเป้าหมายแรก ก็คือต้องเพิ่มรายการลงไป
อันนี้หาง่ายหน่อย อยู่ในกลุ่มงาน Chart of Account
ชื่อหัวข้องานว่า Assign GL Account



ส่วนหน้าจอ listing ก็อยู่ในข้อข้องานว่า List GL Organize



หลังจากใส่ข้อมูลแล้ว ก็ insert ข้อมูลผ่านฉลุย

ขั้นถัดไป ก็คือ อยากทดลอง add บัญชีใหม่ๆเข้าไป
ตรงนี้พบว่า ไม่ว่าจะ click หาหัวข้อใน menu ไหน ก็หาไม่เจอ
สุดท้ายก็ต้องตามไปแกะที่ applications/accounting/webapp/accounting/WEB-INF/controller.xml
พบว่ามีการ define uri ไว้ดังนี้
<request-map uri="AddGlAccount">
<security https="true" auth="true"/>
<response name="success" type="view" value="AddGlAccount"/>
</request-map>

ก็เลยลองเอาไปใส่ url ตรงๆดู
ได้ผลแฮะ ได้หน้าจอมาหนึ่งหน้าจอ




ทดลองป้อนข้อมูลดู กด save
ได้ error มาใหม่อีกหนึ่งตัว
คราวนี้มันบอกว่า
GlAccountId ไม่ได้ระบุ
อ้าว ก็ไม่มี field ให้ป้อนนี่น่า แล้วมันจะระบุที่ไหนหล่ะ

ตามไปดู view definition
<view-map name="AddGlAccount" type="screen" page="component://accounting/widget/AccountingScreens.xml#AddGlAccount"/>

เห็นว่าชี้ไปที่ applications/accounting/widget/AccountingScreens.xml#AddGlAccount
ตามไปดูก็จะเห็น define ไว้แบบนี้

<screen name="AddGlAccount">
<section>
<actions>
<set field="titleProperty" value="PageTitleAddGlAccount"/>
<set field="tabButtonItem" value=""/>
<set field="labelTitleProperty" value=""/>
</actions>

<widgets>
<decorator-screen name="main-decorator" location="${parameters.mainDecoratorLocation}">
<decorator-section name="body">
<include-form name="EditGlAccount" location="component://accounting/webapp/accounting/chartofaccounts/GlAccountForms.xml"/>

<!-- include-screen screen-name="ListGlAccounts" name="ListGlAccounts" / -->

<!-- tree name="GlAccountTree" location="component://accounting/widget/AccountingTrees.xml"/ -->
</decorator-section>
</decorator-screen>
</widgets>
</section>
</screen>

จะเห็นว่ามันเป็นแค่ decorator
ตัว form จริงๆอยู่ใน applications/accounting/webapp/accounting/chartofaccounts/GlAcountForms.xml
<form name="EditGlAccount" type="single" target="updateGlAccount" title="" default-map-name="glAccount">
<alt-target use-when="glAccount==null" target="createGlAccount"/>
<auto-fields-service service-name="updateGlAccount" map-name=""/>

<field use-when="glAccount!=null" name="glAccountId" tooltip="${uiLabelMap.AccountingNotModificationRecrationGlAccount}"><display/></field>
<field use-when="glAccount==null&amp;&amp;glAccountId!=null" name="glAccountId" tooltip="${uiLabelMap.AccountingCouldNotFindGlAccount} [${glAccountId}]"><text size="20" maxlength="20"/></field>
<!-- this to be taken care of with auto-fields-service as soon as it uses entity field info too -->
<field use-when="glAccount==null&amp;&amp;glAccountId==null" name="glAccountId"><text size="20" maxlength="20"/></field>

<field name="glAccountTypeId">
<drop-down allow-empty="false">
<entity-options entity-name="GlAccountType" description="${description}">
<entity-order-by field-name="description"/>
</entity-options>
</drop-down>
</field>
...

อืมม์มัน reuse ใช้ form ทั้งหน้าจอ insert และ update
จะเห็นว่ามันตั้งเงื่อนไขในการแสดง field glAccountId ไว้ดังนี้
1. กรณี glAccount ไม่เป็น null แสดงว่าเป็น mode update
ก็ให้แสดงค่า glAccountId โดยใช้ display tag และอนุญาติให้ดูได้อย่างเดียว ห้ามแก้ไข
2. กรณี glAccount เป็น null และมีหรือไม่มีค่า glAccountId
หน้าจอจะแสดง textfield ให้ใส่

อืมม์ case ของเรามันน่าจะตกข้อ 2, แต่มันทำไม่ไม่แสดง textfield ให้เราใส่
ผมก็เลยไปลองดูในหน้าจอ update ดูว่ามันทำงานอย่างไร
โดยดูใน applications/accounting/widget/AccountingScreens.xml#GlAccountNavigate
ปรากฎว่า flow ก่อนที่จะเข้าหน้าจอ update
มันทำแบบนี้
<screen name="GlAccountNavigate">
<section>
<actions>
...
<!-- parameters includes requestAttributes and parameter map -->
<!-- requestParameters is just the parameter map -->
<set field="glAccountId" from-field="requestParameters.glAccountId"/>
...
<entity-one entity-name="GlAccount" value-name="glAccount"/>
</actions>
<widgets>
...
</widgets>
</section>
</screen>

ลองกลับไปดู flow ในหน้าจอ AddGlAccount มันไม่ได้ set entity นี่หน่า
ทดลองใส่บรรทัดนี้เพิ่มลงไป
<screen name="AddGlAccount">
<section>
<actions>
<set field="titleProperty" value="PageTitleAddGlAccount"/>
<set field="tabButtonItem" value=""/>
<set field="labelTitleProperty" value=""/>
<entity-one entity-name="GlAccount" value-name="glAccount"/>

</actions>
...

ลอง restart OFBiz แล้วเปิด browser ดู
พบว่าได้ผลแฮะ มี textfield ให้ป้อนแล้ว
ทดลองป้อนแล้ว submit ดู ก็พบว่ามันบันทึกลง Database ได้แล้ว

น่าสนใจมาก ใช้ OFBiz ครั้งแรกก็เจอ bug เลย
แต่คิดว่าเป็น bug เนื่องจากเป็นหน้าจอนี้เป็นหน้าจอที่ไม่มีคนใช้ (สังเกตได้จากไม่มี menu ให้เลือก)

Related link from Roti

Monday, July 23, 2007

Max sum ของ Sub-array

zdk post โจทย์ข้อนี้ ใน codenone
เพื่อเป็นการฝึกฝน functional language ผมจึงลองทำด้วย haskell ดู
ทำแล้ว หน้าตาออกมาดูไม่ได้ ก็เลยไป search หาดูว่า บรรดาผู้เชี่ยวชาญ haskell
เข้าทำไว้อย่างไรบ้าง ซึ่งก็สมหวัง เพราะมีการ post ไว้ใน mailing list นี้ Link

ปัญหานี้ ถ้าคิดแบบง่ายสุดก่อน(ไม่คำนึงเรื่อง performance) ก็คือ
1. หา sublist ทั้งหมด
2. คำนวณแปะค่า sum เข้าไปไว้ที่หน้าสุดของ sublist แต่ละตัว
3. หา maximum sum
4. return ค่าที่ได้ โดยโยนค่า sum ทิ้งไป

เริ่มด้วยการหา sublist
ความต้องการคือ
sublist [1,2,3] ต้องได้ [[1],[1,2],[1,2,3],[2],[2,3],[3]] (ไม่จำเป็นต้องเรียงลำดับ)
ดู code ที่ผมเขียน เสียก่อน
sublists xs = sublists' xs [] 
sublists' [] acc = acc
sublists' xs acc = sublists' (tail xs) (tmp ++ acc)
where tmp = snd $ mapAccumL (\acc x -> (x:acc, x:acc)) [] xs

น่าเกลียดมาก เมื่อนำไปเทียบกับมือเก๋าๆแล้ว

เริ่มจากบรรทัดนี้ก่อน เป้าหมายบรรทัดนี้คือ ถ้ามี input เป็น [1,2,3] ต้องได้ [[1],[1,2],[1,2,3]] ออกมา
where tmp = snd $ mapAccumL (\acc x -> (x:acc, x:acc)) [] xs

สามารถใช้ List comprehension แทนได้ดังนี้
where tmp = [ys | n <- [1..length xs], ys <- [(take n xs)]]

ยังยังไม่พอ มี guru มาเฉลยอีกว่า ยาวไป ใช้แค่นี้พอ
where tmp = inits xs

อาฮ้า มี function นี้ให้ใช้ด้วย ตั้งชื่อไม่สื่ออย่างนี้ จะหาเจอได้อย่างไร

ขั้นถัดไปคือปรับตรงบรรทัดนี้
sublists xs = sublists' xs []
sublists' [] acc = acc
sublists' xs acc = sublists' (tail xs) (tmp ++ acc)

เยิ่นเย้อสุดๆ เหลือแค่นี้ก็พอ
sublists [] = []
sublists xs = tmp ++ sublists (tail xs)

อืมม์ได้เท่านี้ก็หรูแล้ว เจอแบบนี้เข้าไปสั้นกว่า
(function tails โผล่ออกมาอีกแล้ว มันมี function แบบนี้ด้วยวุ้ย
tails [1,2,3] = [[1,2,3],[2,3],[3],[]]
)
sublists xs = [zs | ys <- inits xs, zs <- tails ys]

เจ๋งแล้ว แต่มีสั้นกว่านี้อีก
sublists = concatMap tails . inits

แต่การใช้ inits,tails ก็มีปัญหาตรงที่ว่ามันมี empty array เข้ามาปนด้วย
มีคนเสนอ function unfoldr (เอาอีกแล้ว มี function unfoldr ด้วยหรือนี่)
ซึ่งดูดีกว่า ตรงที่ไม่มีปัญหาเรื่อง empty array
แถมยังเป็นการฉลาดที่นำ higher order function ที่มีอยู่แล้วมา apply ใช้
sublists xs = unfoldr f xs
where f [] = Nothing
f xs = Just ([ys | n <- [1..length xs], ys <- [(take n xs)]], tail xs)

หลังจากได้ sublist แล้ว ก็มีถึงขั้นการแปะ sum ลงไปข้างหน้า
ทำง่ายๆโดย
annotate_sum = map (\xs -> (sum xs, xs)) 

จากนั้นก็จัดการหา maximum โดยใช้ function maximumBy
ซึ่งผมเขียนแบบนี้
findMax xs = maximumBy (\a b -> compare (fst a) (fst b)) xs

ก็มีคนมาแสดงให้ดูว่า ทำแบบนี้ได้นะ
findMax xs = maximumBy (\(s1,_) (s2,_) -> compare s1 s2) xs

แล้วก็มีคนมาบอกอีกว่า มันมี function comparing ให้ใช้นะ
(แต่ผมหา function นี้ไม่เจอ คาดว่าคงอยู่ใน version ใหม่กว่าที่มี)
findMax xs = maximumBy (comparing fst)

เมื่อนำมาประกอบกัน ก็ได้ดังนี้
*Main> (snd . findMax . annotate_sum . sublists) [-1,2,5,-1,3,-2,1]
[2,5,-1,3]

Related link from Roti

Wednesday, July 18, 2007

Build flex with Maven

ที่บริษัทฯใช้ maven ในการ build project และควบคุม dependency
ตอนนี้เริ่มมี flex component เข้ามาปะปนใน java project ด้วย
ก็เลยอยากให้ process กับ project structure ที่ใช้พัฒนา flex
เป็นมาตรฐานเดียวกัน

search ดูใน google พบว่ามีคนทำ plugin อยู่ 2 ราย
แต่รายที่ลองแล้ว work ก็คือ
maven-flex2-plugin

เป้าหมายของ maven-flex2-plugin ก็คือ
การช่วย build และ สร้าง package swc (flex library) หรือ swf (flex application) ให้เรา
การใช้งาน ก็เริ่มด้วยการสร้าง pom.xml
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>cnug</groupId>
<!-- ชื่อของ artifact -->
<artifactId>hangman</artifactId>
<version>1.0</version>
<!-- ชนิดของ package -->
<packaging>swf</packaging>
<properties>
<flex.home>/home/pphetra/dev/flex</flex.home>
</properties>
<!-- PLUGIN REPOSITORIES -->
<pluginRepositories>

<pluginRepository>
<id>central</id>
<name>Central Maven Repository</name>
<url>http://repo1.maven.org/maven2</url>
</pluginRepository>

<pluginRepository>
<id>maven-snapshots</id>
<name>Maven Snapshots Repository</name>
<url>http://snapshots.maven.codehaus.org/maven2</url>
</pluginRepository>

<pluginRepository>
<id>israfil</id>
<name>israfil Repository</name>
<url>http://www.israfil.net/maven2</url>
</pluginRepository>

</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>net.israfil.mojo</groupId>
<artifactId>maven-flex2-plugin</artifactId>
<version>1.0</version>
<extensions>true</extensions>
<configuration>
<useNetwork>true</useNetwork>
<strict>true</strict>
<warning>true</warning>
<!-- application mxml file -->
<main>main.mxml</main>
</configuration>
</plugin>
</plugins>
</build>
</project>


ส่วน project structure, เราจะวาง code ของ flex ไว้ใน
src/main/flex

ตัวอย่าง application hangman ที่ผมลองสร้างดู
ผมวาง code ไว้ใน directory แบบนี้

+ src
+ main
+ flex
- main.mxml
+ assets
- win.png
- lose.png
- mistake0.png
...
+ hangman
+ core
- Game.as
+ ui
- GameGraphics.as
- Unreveal.as
- Letters.as

Related link from Roti

Friday, July 13, 2007

Lists and Lists

Lists and Lists
อาฮ้า เกมส์นี้เขียนขึ้นเมื่อปี 1996 เป็นเกมส์ประเภท IF (interactive fiction)
แต่จุดเด่นของเกมส์นี้ก็คือ มันเป็น programming tuturial ด้วย
ใครอยากรู้จัก scheme ลองเล่นเกมส์นี้ดูได้

Note: ที่ลง link ไว้เป็น java applet ที่เข้าเล่นเกมส์เลย
แต่ถ้าใครอยาก download ไว้เล่นที่เครื่องตัวเอง
ต้องไปที่นี่ Link

Related link from Roti

Thursday, July 12, 2007

FlexUnit

เมื่อวานนั่งทดลองเขียนเกมส์ Hangman บน Flex ดู
สิ่งที่ขัดอกขัดใจก็คือ มันไม่มี Unit Testing มาให้

ซึ่งก็จริงของมันในแง่หนึ่ง ก็คือ
component ที่เป็น UI component
มันทำ testing แบบ Unit ได้ยาก (ก็เลยไม่มีเสียเลย แต่ก็ไปแอบมี third-party ทำ GUI Testing plugin แทน)
แต่เผอิญว่า component ของเรา มันเป็น Non-visual component
และต้องการทดสอบความถูกต้อง ก่อนที่จะ integrate กับ UI component

จากการ search พบว่า มันมี library ที่ชื่อ FlexUnit อยู่เหมือนกัน
(อยู่ใน adobe labs project ด้วย)
ก็เลย load มาทดสอบดู

การใช้งาน ก็คือ สร้าง MXML Application ขึ้นมาตัวหนึ่ง
มีหน้าตาแบบนี้

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:flexunit="flexunit.flexui.*"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[
import flexunit.framework.TestSuite;
import hangman.test.GameTest;

// After everything is built, configure the test
// runner to use the appropriate test suite and
// kick off the unit tests
private function onCreationComplete():void
{
testRunner.test = createSuite();
testRunner.startTest();
}

// Creates the test suite to run
private function createSuite():TestSuite {
var ts:TestSuite = new TestSuite(GameTest);
return ts;
}

]]>
</mx:Script>

<!-- flexunit provides a very handy default test runner GUI -->
<flexunit:TestRunnerBase id="testRunner" width="100%" height="100%" />
</mx:Application>


ตัว TestCase ก็เขียนเหมือน XUnit ทั่วๆไป

package hangman.test
{
import flexunit.framework.TestCase;
import hangman.core.Game;

public class GameTest extends TestCase
{
public function testNewGame():void
{
var game:Game = new Game();
game.word = "hello";
assertEquals("_____", game.unreveal);
}

public function testGuess():void
{
var game:Game = new Game();
game.word = "hello";
game.guess("h");
assertEquals("h____", game.unreveal);
game.guess("l");
assertEquals("h_ll_", game.unreveal);
}

public function testGuessMistake():void
{
var game:Game = new Game();
game.word = "hello";
game.guess("a");
assertEquals(1, game.mistake);
game.guess("b");
assertEquals(2, game.mistake);
assertEquals("_____", game.unreveal);
}
}
}


เวลา run ก็ใช้ได้ผลลัพท์ออกมาใน browser แบบนี้



ข้อสังเกต
1. unit test ไม่ได้ integrate ใน flex-builder ทำให้เราไม่สามารถกระโดดจากผลลัพท์
ไปยัง source code ได้โดยตรง
2. กรณีที่มี testcase, flex ยังไม่มี best-practice ของการจัด Project Layout
ว่าควรจะเขียน testcase แยกเป็นอีก project หรือ รวมไว้ใน project เดียวกัน
ถ้ารวมจะจัด package หรือ folder structure อย่างไรดี

Related link from Roti

Tuesday, July 10, 2007

ทดลอง implement widget บน GWT

ช่วงนี้ผมกำลังทดลองเขียนโปรแกรม hangman
เพื่อจะได้ทดสอบ framework ต่างๆเปรียบเทียบกัน
โดยต้นแบบของเกมส์ ได้มาจาก code ตัวอย่างใน cincom smalltalk
ที่ใช้ seaside เป็น framework

บน game Hangman, user สามารถกดเลือกตัวอักษรที่จะทายได้
โดยตัวอักษรพวกนี้ จะ render เป็น hyperlink ไว้
และเมื่อ user กดเลือกไปแล้ว, hyperlink นั้นก็จะกลายเป็น text ธรรมดา
เพื่อเป็น feedback ให้ user รู้ว่าได้ทำการเลือกไปแล้ว



ผมลอง implement feature ที่ว่าเป็น widget ใน GWT ดู
เกริ่นก่อนว่า, ใน GWT ก็มี widget ประเภท hyperlink ให้ใช้
แต่มีข้อจำกัดว่า ไม่สามารถทำให้มันกลายเป็น text ธรรมดาได้
เราก็เลยต้อง customize ทำ hyperlink แบบพิเศษของเราขึ้นมาเอง

ทางเลือกในการ implement hyperlink แบบพิเศษของเรา มีอยู่ 2 ทางคือ
1. extends จาก hyperlink ของเดิม แล้ว overide method บางตัว เพื่อปรับพฤติกรรม
2. เขียนขึ้นมาเอง โดยไม่ยุ่งกับ HyperLink widgetเลย
ผมเลือกใช้วิธีที่ 2 เพื่อจะได้ทำความเข้าใจกับกลไกการทำงานของ GWT มากขึ้น

เริ่มแรกสุด ก็คือเลือก extend widget ของเราจาก FocusWidget,
โดย FocusWidget คือ base class สำหรับ widget ที่สามารถ click
หรือเรียกใช้ผ่าน access key ได้
public class Letter extends FocusWidget {

}

ขั้นถัดมา ก็คือ implement ส่วนที่เกี่ยวกับ การ render
วิธีการที่ GWT ใช้ render widget ก็คือ render เป็น DOM element
public class Letter extends Widget {
private Element anchorElem;
private char ch;

public Letter(char ch) {
this.ch = ch;
setElement(DOM.createSpan());
DOM.appendChild(getElement(), anchorElem = DOM.createAnchor());
DOM.setInnerText(anchorElem, Character.toString(ch));
DOM.setElementProperty(anchorElem, "href", "#");
}
}

ผลลัพท์ที่ได้จากข้างบน ก็คือเราจะได้ html element ดังนี้
<span><a href="#"></a></span>


ข้อสงสัยก็คือ ถ้า render แค่นี้แล้ว GWT จะรับ javascript event onclick ได้อย่างไร
คำตอบก็คือ ถ้าเราไปไล่ code ของ FocusWidget ดู
เราจะเห็นว่าหลังจากที่เราสั่ง setElement
code ใน FocusWidget จะทำการเรียกใช้
sinkEvents(Event.ONCLICK | Event.FOCUSEVENTS | Event.KEYEVENTS);

ซึ่งเป็นการระบุว่า widget เรา สามารถรับ event กลุ่ม click, focus, keyevent ได้
โดย callback ที่ต้อง implement เพื่อรับ event ก็คือ method onBrowserEvent
ในกรณีของเรา FocusWidget มีการ implement onBrowserEvent ไว้ดังนี้แล้ว
public void onBrowserEvent(Event event) {

switch (DOM.eventGetType(event)) {
case Event.ONCLICK:
if (clickListeners != null) {
clickListeners.fireClick(this);
}
break;
...
}
}

สิ่งที่เราต้องการ overide ก็คือ เมื่อเกิด clickEvent แล้ว
เราต้องการให้ hyperlink ของเรา click อีกไม่ได้
ซึ่งทำง่ายๆโดย เอา attribute href ออกจาก anchor tag
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
if (DOM.eventGetType(event) == Event.ONCLICK) {
DOM.removeElementAttribute(anchorElem, "href");
DOM.eventPreventDefault(event);
}
}


เมื่อได้ตัวอักษรเดี่ยวๆแล้ว ก็มาลองทำ widget ที่ช่วย render ชุดตัวอักษรบ้าง
โดย widget นี้เราจะนำเอา Letter widget ที่เราพึ่งทำไปมาเรียงต่อๆกัน
ในกรณีนี้ widget ที่เราจะเขียน จะ extend จาก Composite
public class Letters extends Composite implements ClickListener {

private Letter[] letters;

public Letters(String chars) {
FlowPanel panel = new FlowPanel();
char[] chs = chars.toCharArray();
letters = new Letter[chs.length];
for (int i = 0; i < chs.length; i++) {
letters[i] = new Letter(chs[i]);
letters[i].addClickListener(this);
panel.add(letters[i]);
panel.add(new Space(1));
}
initWidget(panel);
}

...

}


ความรู้สึกที่ได้จากการทดลอง implement widget ใน GWT ก็คือ
1. ความรู้สึกก้ำกึ่ง ระหว่างความชอบ กับ ไม่ชอบ
เนื่องจาก บางอย่าง ถ้าเราเขียนด้วย javascript โดยตรง มันจะง่ายกว่า
แต่บางอย่าง เขียนด้วย GWT ก็จะง่ายกว่า (เช่น sinkEvent)
2. GWT มันไม่ support feature ของ Java 1.5
ทำให้รู้สึกเย่นเย้อเวลาเขียนบางอย่าง เช่นพวก for loop
หรือต้องใส่ casting เวลา access collection

Related link from Roti

Monday, July 09, 2007

LEL

โปรเจค Profligacy เป็นโปรเจค ที่ช่วยให้เรา paint swing component
โดยใช้ JRuby เข้ามาช่วย
สิ่งที่น่าสนใจในโปรเจคนี้ก็คือแนวคิดเรื่อง LEL (Layout Expression Language)
โดยนำแนวคิดของเรื่อง table format ในพวก wiki มาผสมกับ Constraint programming
ก็เลยได้ออกมาเป็นวิธีใหม่ในการวาง layout

ลองดูตัวอย่าง code
require 'profligacy/swing'
require 'profligacy/lel'

module Test
include_package 'javax.swing'
include Profligacy

layout = "
[ label_1 | label3 ]
[ (300,300)*text1 | (150)people ]
[ <label2 | _ ]
[ message | buttons ]
"


ui = Swing::LEL.new(JFrame,layout) do |c,i|
c.label_1 = JLabel.new "The chat:"
c.label2 = JLabel.new "What you're saying:"
c.label3 = JLabel.new "The people:"
c.text1 = JTextArea.new
c.people = JComboBox.new
c.message = JTextArea.new

c.buttons = Swing::LEL.new(JPanel, "[send|hate|quit]") do |c,i|
c.send = JButton.new "Send"
c.hate = JButton.new "Hate"
c.quit = JButton.new "Quit"
end.build :auto_create_container_gaps => false
end

ui.build(:args => "Simple LEL Example")
end


จะเห็นว่า main format เขาวางไว้แบบนี้
[ label_1         | label3      ]
[ (300,300)*text1 | (150)people ]
[ <label2 | _ ]
[ message | buttons ]


คำอธิบายก็คือ layout จะเป็น grid ที่มี 4 แถว 2 column,
< หมายถึง ชิดซ้าย
> หมายถึง ชิดขวา
* คือ expand ให้เต็มพื้นที่
_ หมายถึง empty cell
(150) หมายถึง กว้าง 150 pixel
(300,300) กว้าง 300 ยาว 300 pixel

ผลลัพท์ที่ได้ ลองตามไปดูที่นี่
http://ihate.rubyforge.org/profligacy/images/sample_lel_gui_nested.png

Related link from Roti