Friday, March 18, 2005

จังหวัด-อำเภอ Component in Tapestry

เมื่อก่อนผมใช้ Struts เป็น Framework สำหรับ Web Application
ซึ่งก็ใช้ได้ดีทีเดียวแหล่ะ แต่มีอยู่จุดหนึ่งที่รู้สึกว่าเป็นจุดอ่อน
ของ struts ก็คือประเด็นเรื่อง reuse UI component
จริงๆอยู่เราสามารถทำ custom taglib ในส่วนนี้เองได้
แต่มันก็ยังขาดๆเกินๆอยู่

จนมาได้ลองใช้ Tapestry จึงได้พบคำตอบในส่วน UI Component
Tapestry เป็น Framework ที่มีรากฐานอยู่บน
Component เราจึงสามารถ reuse component
ต่างๆได้เต็มเม็ดเต็มหน่วย (แน่นอนมีสิ่งที่ต้องแลกเปลี่ยนเหมือนกัน)

framework ของ Tapestry เปิดโอกาสให้เรา
ได้ทำ binding ทั้ง 2 ทาง คือ ทั้งขา Request และ
ขา Response ซึ่งต่างจาก Taglib ที่ทำ binding
เฉพาะในส่วนการ Render อย่างเดียว (เมื่อ user post กลับมา
ตัว taglib จะไม่ได้ถูกเรียกใช้ แต่กรณีถ้าเป็น tapestry
ตัว component จะถูกเรียกใช้ ซึ่งในการเรียกใช้ครั้งนี้
ไม่ได้เพื่อทำการ render แต่เพื่อการ parse request parameter)

ลองมาดูกันว่า กรณีที่เป็น Tapestry Component แล้ว
เราต้องทำอะไรบ้าง

อำเภอ จังหวัด component
หลายคนคงเคยทำ ui ในส่วน "ที่อยู่" ที่ให้ user ป้อนมาแล้ว
หลักการง่ายๆก็คือ มี list box จังหวัด กับ list box อำเภอ
เมื่อ user เลือกจังหวัด list box ของอำเภอก็จะเปลี่ยน choice ตาม
การเปลี่ยนค่านี้กำหนดให้ใช้ XmlHttpRequest เพื่อไม่ให้เกิดการ
update page ทั้งหมด
โดยผมจะลองแสดงให้ดูว่าถ้าเป็น tapestry component
แล้วจะต้องทำอย่างไร

กำหนดให้ชื่อ component ว่า ZoneComponent

Component Specification
ตั้งชื่อว่า ZoneComponent.jwc มีหน้าตาดังนี้
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE component-specification
PUBLIC "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<!-- generated by Spindle, http://spindle.sourceforge.net -->

<component-specification
class="sdf.web.comps.ZoneProvince"
allow-body="no" allow-informal-parameters="yes">

<parameter name="province" direction="form"
type="sdf.domain.Province" required="yes"/>
<parameter name="zone" direction="form"
type="sdf.domain.Zone" required="yes"/>
<parameter name="serviceUrl" direction="in"
type="java.lang.String" required="yes"/>
<parameter name="serviceMethod" direction="in"
type="java.lang.String" required="yes"/>
<parameter name="disabled" direction="in"
type="boolean" required="no"/>

<property-specification name="name"
type="java.lang.String"/>
<property-specification name="form"
type="org.apache.tapestry.IForm"/>

<private-asset name="script"
resource-path="/sdf/web/comps/ZoneProvince.script"/>
</component-specification>

วัตถุประสงค์หลักๆของ component-spec ก็คือ
การกำหนดว่า component เรามี parameter อะไรให้ set ได้บ้าง
(คนที่ set ก็คือ คนที่เอา component เราไปร้อยเข้าเป็น Application)
ตัวที่น่าสนใจก็คือ parameter name="province"
type="sdf.domain.Province" -> อันนี้คือ business object ของจังหวัด
direction="form" -> ระบุว่าเป็น bi-direction ใช้ทั้งกรณี Request และ Response
ส่วนตัว serviceUrl, serviceMethod เป็นตัวกำหนดค่า url ของ service
ที่ให้บริการกรณีที่ user click เปลี่ยนจังหวัดและต้องการนำค่า อำเภอใหม่
ไป update "อำเภอ listbox" ค่าพวกนี้มี Direction เป็น "in"
เนื่องจากเป็นพวก readonly ไม่มีการ post back กลับมาจาก user

JavaScript Template
เนื่องจากเราใช้ XMLHttpRequest เป็น channel ในการ update listbox
ดังนั้นต้องมีการใช้ javascript เข้ามาช่วย
Tapestry วางแนวทางการ embed javascript ไว้แล้วโดย
ให้เราใช้ Javascript template ในการ define javascript
(เนื่องจาก component เราจะถูกประกอบทีหลัง ดังนั้นจะมี
ค่าบางค่าที่ยังไม่รู้ว่าจะเป็นอะไร อย่างเช่นชื่อของ component
จึงต้องออกแบบให้เป็น template
เมื่อรู้ค่าที่แน่นอนแล้วจึงจะสามารถสร้างออกมาเป็น javascript
ที่ใช้งานได้จริงๆ การทำเช่นนี้ทำให้ไม่เกิดปัญหา naming comflict
กรณีที่เรามี instance ของ component มากกว่า 1 instance ใน
page เดียวกัน เช่นกรณีที่อยู่ที่ใน 1 หน้าอาจมีทั้ง ที่ทำงาน ที่ติดต่อ สถานที่เกิด)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script PUBLIC
"-//Apache Software Foundation//Tapestry Script Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Script_3_0.dtd">

<script>
<include-script resource-path="jsrsClient.js"/>

<input-symbol key="provinceName"
class="java.lang.String" required="yes"/>
<input-symbol key="zoneName"
class="java.lang.String" required="yes"/>
<input-symbol key="formName"
class="java.lang.String" required="yes"/>
<input-symbol key="serviceUrl"
class="java.lang.String" required="yes"/>
<input-symbol key="serviceMethod"
class="java.lang.String" required="yes"/>
<input-symbol key="scriptChangedMethod"
class="java.lang.String" required="yes"/>

<let key="populateZone" unique="yes">
populateZone_${zoneName}
</let>


<body>
function clearDropDown (selField)
{
while (selField.options.length > 0)
selField.options[0] = null;
}

function ${populateZone} (valueTextStr)
{
var selField = document.${formName}.${zoneName};
clearDropDown(selField);

// create an array of each item for the dropdown
var aPairs = valueTextStr.split(";");
if (valueTextStr.substr(valueTextStr.length - 1) == ';') {
aPairs[aPairs.length - 1] = null;
aPairs.length--;
}
// Fill 'er up
for (var i=0; i <; aPairs.length; i++) {
// each item is a "value,name" pair
aValueText = aPairs[i].split(",");
oItem = new Option;
oItem.value = aValueText[0];
oItem.text = aValueText[1];
selField.options[selField.options.length] = oItem;
}
selField.options.selectedIndex = 0;
}

function ${scriptChangedMethod}()
{
jsrsExecute("${serviceUrl}", ${populateZone},
"${serviceMethod}",
document.${formName}.${provinceName}
.options[document.${formName}
.${provinceName}.selectedIndex].value);
}
</body>

จะเห็นว่ามี element หลักๆ อยู่ 3 กลุ่ม
  • <include-script> ตัวนี้จะเป็นตัวบอกว่ามี js library ตัวไหนที่ต้อง load มาบ้าง
    กรณีของเราเราใช้ jsrsClient.js

  • <input-symbol> ใช้กำหนดค่าตัวแปรที่ต้องถูก pass เข้ามา ณ ตอนที่
    generate javascript

  • <body> เป็น script หลักในการทำงาน
    จะเห็นได้ว่าตัวแปรที่ยังไม่รู้ค่าจะอยู่ในรูป ${xxxxx}


Component Class
ตรงนี้จะเขียน code หลักในส่วนของการ render และ
การ parse Request parameter เมื่อ user post form กลับเข้ามา

  • กรณีการ render component จะหน้าตาประมาณนี้

  Body body = Body.get(cycle);

if (body == null)
throw new ApplicationRuntimeException(
Tapestry.format("must-be-contained-by-body",
"ZoneProvince"),
this,
null,
null);

Map symbols = new HashMap();

symbols.put(SYM_PROVINCE_NAME, provinceName);
symbols.put(SYM_ZONE_NAME, zoneName);
symbols.put(SYM_FORMNAME, form.getName());
symbols.put(SYM_SERVICE_METHOD, getServiceMethod());
symbols.put(SYM_SERVICE_URL, getServiceUrl());
symbols.put(SYM_SCRIPT_CHANGED_METHOD, scriptChangedMethod);

// generate ส่วน JavaScript
_script.execute(cycle, body, symbols);


writer.begin("select");
writer.attribute("name", zoneName);
renderInformalParameters(writer, cycle);

String provCode = "10"; // default to bangkok
if (getProvince() != null) {
provCode = getProvince().getCode();
}
String zoneCode = null;
if (getZone() != null) {
zoneCode = getZone().getCode();
}

// render zone list
List list = svr.findZone(provCode,
IConstants.ORDER_BY_NAME);
for (Iterator iter = list.iterator(); iter.hasNext();) {
Zone zone = (Zone) iter.next();
writer.begin("option");
writer.attribute("value", zone.getCode());
if (zoneCode != null &&
zone.getCode().equals(zoneCode)) {
writer.attribute("selected", true);
}
writer.closeTag(); // close option tag
writer.printRaw(zone.getName());
writer.end("option");
}
writer.end("select");

// render province list
writer.begin("select");
writer.attribute("name", provinceName);
writer.attribute("onchange",
"javascript:" + scriptChangedMethod + "()");
renderInformalParameters(writer, cycle);

list = svr.findAllProvince(IConstants.ORDER_BY_NAME);
for (Iterator iter = list.iterator(); iter.hasNext();) {
Province province = (Province) iter.next();
writer.begin("option");
writer.attribute("value", province.getCode());
if (province.getCode().equals(provCode)) {
writer.attribute("selected", true);
}
writer.closeTag(); // close option tag
writer.printRaw(province.getName());
writer.end("option");
}

writer.end("select");


  • กรณีการ parse parameter เมื่อมีการ post กลับมา

  String provinceCode = cycle.getRequestContext()
.getParameter(provinceName);
String zoneCode = cycle.getRequestContext()
.getParameter(zoneName);

if (Tapestry.isBlank(provinceCode)) {
return;
} else {
Province value = svr.getProvince(provinceCode);
setProvince(value);
}

if (Tapestry.isBlank(zoneCode)) {
return;
} else {
Zone value = svr.getZone(zoneCode);
setZone(value);
}

มี code หลักๆ เท่านี้ก็ได้ component สำหรับ reuse ใช้ใน Application
เราแล้ว คราวนี้ไม่ว่าจะเป็น ที่อยู่, ที่ติดต่อ, บ้านเกิด, ที่ทำงาน ก็สามารถ
ใช้ component ชุดเดียวกันได้

ตัวอย่างเวลานำไปใช้
  • html file

    <td valign="top" class="label"><div 
align="right">ที่อยู่ :</div></td>
<td><table width="100%" border="0">
<tr>
<td><input jwcid="addr1@ValidField"
value="ognl:firm.address.addr1"
displayName="ที่อยู่"
validator="ognl:beans.addr1RequireValidator"
type="text" size="50" maxlength="50"/></td>
</tr>
<tr>
<td><input jwcid="@TextField"
value="ognl:firm.address.addr2"
type="text"
size="50"
maxlength="50"/></td>
</tr>
<tr>
<td><span jwcid="zoneProvince">
zone components
</span></td>
</tr>
</table></td>

จะเห็นว่าใช้แค่ jwcid="zoneProvince" เป็นการบอกว่า
ตรงนี้เป็น component ที่มีชื่อ instance ว่า zoneProvince
ซึ่งจะมีการ declare ไว้ใน Page Specification อีกที
  • Page Specification file

    <component id="zoneProvince" type="ZoneProvince">
<binding name="province"
expression="firm.address.province"/>
<binding name="zone"
expression="firm.address.zone"/>
<static-binding name="serviceUrl"
value="/sdf/zone"/>
<static-binding name="serviceMethod"
value="getZone"/>
</component>

จะเห็นว่าตรงนี้จะมีการ binding parameter เข้ากับ Page Object
โดยระบุตรง expression คำว่า "firm.address.province" หมายถึง
ให้ไปเอา object provice มาโดยใช้คำสั่ง
getFirm().getAdress().getProvince() กับ Page Object

Note: ต้วอย่างการนำไปใช้ที่แสดงให้ดู
เป็นการเขียนแบบ formal ถ้าเป็นแบบ informal code
จะกระทัดรัดกว่านี้ แต่จะอ่านยากกว่าถ้าไม่เคยใช้
tapestry มาก่อน


ข้อมูลเพิ่มเติม

Related link from Roti

SpringDoc

ช่วย Generate Document ของ Spring Context Xml File
Spring Doc
เยี่ยมมาก ช่วยให้เรา visualize โครงสร้างได้ชัดขึ้น

Related link from Roti

XMLHttpRequest & Ajax Working Examples

XMLHttpRequest & Ajax Working Examples - Links and Resources, Fiftyfoureleven.com

Related link from Roti

Wednesday, March 16, 2005

Ruby Quiz

ชอบวิธีที่เขาอธิบายโจทย์
และเฉลย อธิบายชัดเจนดี
เขียน ruby ไม่เป็นก็ดูได้ครับ (ผมก็เขียนไม่เป็น)
Ruby Quiz

Related link from Roti

ทดลอง Blogger API

ขอพักส่วน UI ของ LogAnalyzer ไว้ก่อน
เมื่อวานได้ปั่น version java swing ให้พอใช้งานได้เสร็จไปแล้ว
ไว้มีเวลาแล้วจะทดลองในส่วนของ jface, swt ต่อ

ย้อนกลับไปเมือวันอาทิตย์ได้ทดลองใช้ blogger api ดู
สืบเนื่องจากไม่ค่อยถูกใจ editor ที่ใช้ในปัจจุบัน
ก็เลยคิดจะหาทาง post เอง

เริ่มจากเข้าไปที่หน้า http://www.blogger.com/developers/
จะเห็นว่ามี api ให้เลือกอยู่ 2 ตัวก็คือ
  • Blogger API 1.0
  • Atom API


Blogger API
API ตัวนี้ใช้ XML-RPC ก็เลยไป load เอา
Apache XML-RPC มาทดลองใช้
เขียน Code แค่นี้ก็ post ได้แล้ว
  XmlRpcClient client = new XmlRpcClient
("http://plant.blogger.com/api/RPC2");
Vector args = new Vector();
args.add("0123456789ABCDEF"); // APP ID ปัจจุบัน blogger ไม่สนใจค่านี้แล้ว
args.add("11409654"); // blog id
args.add("pphetra"); // user id
args.add("***********"); // password
args.add("content .........");
args.add(Boolean.TRUE); // publish
try {
client.execute("blogger.newPost", args);
System.out.println(obj.getClass().getName());
} catch (Exception e) {
e.printStackTrace();
}

ง่ายดายมาก แต่ก็เจอปัญหาอยู่ 2 จุด
จุดที่ 1 คือ ไม่มี param blog title ให้ใช้
ค้นหาเอกสารพบว่า มีส่วนขยายที่ชื่อ MetaWebLog
ให้ใช้ (ไม่ได้ทดลองเพราะยังแก้ปัญหาจุดที่ 2 ไม่ได้)
จุดที่ 2 อันนี้ร้ายแรงหน่อย ก็คือ ส่งภาษาไทยไม่ได้
พอทดลองส่งภาษาไทยพบปัญหา
java.io.IOException: Invalid character data corresponding to XML entity ก
at org.apache.xmlrpc.XmlRpcClient$Worker.execute(XmlRpcClient.java:444)
at org.apache.xmlrpc.XmlRpcClient.execute(XmlRpcClient.java:163)

ก็เลยไปดู source code พบว่า
default:
if (c < 0x20 || c > 0xff)
{
// Though the XML-RPC spec allows any ASCII
// characters except '<' and '&', the XML spec
// does not allow this range of characters,
// resulting in a parse error from most XML
// parsers.
throw new XmlRpcClientException("Invalid character data " +
"corresponding to XML entity &#" +
String.valueOf((int) c) + ';', null);
}
else
{
write(c);
}
}

ก็เลยสงสัยว่า แล้วพวกประเทศอื่นๆไม่มีใครเขาใช้ apache xml-rpc เลยหรือ
ก็เลยเข้าไปค้น issue ดูพบ
http://issues.apache.org/jira/browse/XMLRPC-45
ค่อยรู้สึกว่ามีเพื่อนหน่อย
จากนั้นก็เลยทำการ comment ประโยคที่ตรวจสอบออก
แล้วก็ build xml-rpc เอง
คราวนี้ส่งได้แล้ว แต่ฟัง blogger รับแล้วแสดงผลเป็น ?????
ก็เลยหมดปัญญา เพราะไม่รู้ว่าโปรแกรมฝั่งเขามีขบวนการ decode
อะไรแบบไหน ก็เปลี่ยนมาใช้ Atom api

Atom API
ตาม link ข้างบนไปจตถึงหน้านี้ http://atomenabled.org/developers/api/
เห็นมี WSDL ก็เลยกะว่าจะใช้ Apache AXIS
Generate Client code ให้
ทำการสั่ง wsdl2java พบปัญหา
C:\temp>java org.apache.axis.wsdl.WSDL2Java
http://atomenabled.org/developers/api/AtomAPI.wsdl
java.util.NoSuchElementException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:785)
at java.util.HashMap$ValueIterator.next(HashMap.java:812)

ค้นใน net พบ issue AXIS-249
ตกลงเป็นปัญหาที่ axis ไม่สามารถ generate operation ที่ไม่มี parameter ได้
(ดูแล้ว axis ก็ถูกนะจะมี web service ที่ไหน ที่ไม่ต้อง pass param ไปบ้าง)
คราวนี้ขี้เกียจ build axis ใหม่ ก็เลยใช้วิธี load เอา wsdl มาแก้ไข
โดยลบส่วน operation ที่ไม่มี parameter ออก
คราวนี้สามารถ generate source code ออกมาได้แล้ว
ทดลอง เขียนโปรแกรมดู
  AtomAPILocator locate = new AtomAPILocator();
AtomAPISoap api = locate.getAtomAPISoap(new
URL("http://www.blogger.com/atom/11409654"));
ContentType ct = new ContentType();
ct.setMode(ContentTypeMode.xml);
ct.setAny(.... (source code ตรงนี้แก้ไปแล้วเลยจำไม่ได้));

EntryType entry = new EntryType();

entry.setTitle("hello test");
entry.setIssued(new GregorianCalendar());
entry.setContent(new ContentType[] {ct});
api.POST(entry);

ส่งครั้งแรกไม่ผ่าน ติดปัญหา authenticate
ก็เลยไปแก้ source code ที่ axis gen ให้ เพิ่มส่วน
set user กับ password ลงไป
  org.apache.axis.client.Call _call = createCall();
_call.setUsername("pphetra");
_call.setPassword("*********");

คราวนี้ผ่าน step แรกแต่พบปัญหาว่ามันฟ้องเรื่อง
Malformed XML. Mode indicated was 'xml' but data was not xml

ก็เลยต้อง start tcpmon เพื่อดูว่า protocol ที่ส่งไปมีหน้าตาเป็นอย่างไร
ได้ความว่ามันส่งส่วน post แบบนี้
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<POST xmlns="http://schemas.xmlsoap.org/wsdl/http/">
<ns1:entry xmlns:ns1="http://purl.org/atom/ns#">
<ns1:title>hello test</ns1:title>
<ns1:generator xsi:nil="true"/>
<ns1:issued>2548-03-16T09:22:18.640Z</ns1:issued>
<ns1:content></ns1:content>
</ns1:entry>
</POST>
</soapenv:Body>
</soapenv:Envelope>

ก็เลยสงสัยว่าตัว content มันหาย
ก็เลยไป hack ส่วน code ที่ axis generate ออกมา
เขียนส่วน serializable ของ content เอง
ในที่สุดก็ได้ content ออกมา
แต่พอ run แล้วก็ยังได้ message เหมือนเดิม

ชักหมดแรง ลองมาหลายวิธีจนเหนื่อยแล้ว
(หมกมุ่นจนภรรยาเกิดอาการงอน)
พักไว้ก่อนดีกว่า

ปล. วันนี้ค้นเจอแล้วว่า format ที่ถูกเป็นอย่างไร
ตัวเอกสารอยู่ที่ atom-docs
ไม่เห็นมี SOAP แปะเลย แล้วมันจะมี WSDL ไว้ทำหยัง
ไว้ให้แฟนอารมณ์ดีแล้วค่อยลองใหม่

POST /atom/3187374 HTTP/1.1
Host: www.blogger.com
Authorization: BASIC c3RldmVqOm5vdGFyZWFscGFzc3dvcmQ=

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<entry xmlns="http://purl.org/atom/ns#">
<title mode="escaped" type="text/plain">atom test</title>
<issued>2004-04-12T06:07:20Z</issued>
<generator url="http://www.yoursitesurlhere.com">Your client's name here.</generator>
<content type="application/xhtml+xml">
<div xmlns="http://www.w3.org/1999/xhtml">Testing the Atom API</div>
</content>
</entry>

Related link from Roti

Tuesday, March 15, 2005

Java Programming Notes

วันนี้เขียน swing มีบางเรื่องเป็นเรื่องเล็กน้อยที่จำไม่ได้
ไปค้นเจอใน note นี้
Java Programming Notes
ีช่วยเตือนความจำได้ดี

Idea
ถ้า load มาแล้วเอา lucene มาช่วยทำ index
integrate เข้าเป็น plugin ของ eclipse

Related link from Roti

dojo.io.bind(): baking usability into XMLHTTP

dojo.io.bind(): baking usability into XMLHTTP

Related link from Roti

Monday, March 14, 2005

Project Log Analyzer #2

เมื่อวาน implement ส่วน Log Parser เสร็จไปแล้ว
วันนี้ก็จะเริ่มทำส่วน Index Repository กับส่วน Query บ้าง

Index Repository
Index Repository ก็คือ พื้นที่ที่เก็บ mapping
ระหว่าง label กับ rownumber ของ log file

ในส่วนนี้เราจะนำเอา class BitSet มาใช้
implement ง่ายๆ ดังนี้
Map labelMap;
public void addLabel(String labelId,
String labelValue, int rowNumber) {
BitSet bs = null;
if (labelMap.containsKey(labelValue)) {
bs = (BitSet) labelMap.get(labelValue);
} else {
bs = new BitSet();
labelMap.put(labelValue, bs);
}
bs.set(rowNumber);
}

Query
แยกทำเป็น 2 ส่วนคือ Basic Search กับ Advance Search

Basic Search
เป็นการค้นหาแบบง่่ายๆ ตรงไปตรงมา
user ใส่ข้อมูลในลักษณะเป็น "word (word)*"
เช่น
"sompop" -> ค้นหา line ที่มี label = sompop
"sompop uc2110" -> ค้นหา line ที่มี label = sompop และ = uc2110
basic search implement ได้ง่ายๆดังนี้
public int[] search(String[] labelValues) {
BitSet bs = null;
for (int i = 0; i < labelValues.length; i++) {
BitSet tmp = (BitSet) repo.labelMap.get
(labelValues[i]);
if (tmp != null) {
if (bs == null) {
bs = (BitSet) tmp.clone();
} else {
bs.and(tmp);
}
}
}
return converToArray(bs);
}

Advance Search
user สามารถใส่ข้อมูลในลักษณะ logical expression ได้เช่น
"sompop & (piya | uc2110)"
การ implement เลือกใช้ Antlr มาทำเป็น parser
เนื่องจากรู้สึกลืมๆไปหมดแล้ว เลยอยากทบทวน

Lexer
มี spec ดังนี้
class AdvQryLexer extends Lexer;
options {
k = 2;
}

AND : '&';
OR : '|';
LPAREN : '(';
RPAREN : ')';
NEGATE : '!';
SEMI : ';';

WS : ( ' '
| '\t'
| "\r\n"
| '\n'
) {$setType(Token.SKIP);}
;

protected
LETTER
: '\u0024' |
'\u0041'..'\u005a' |
'\u005f' |
'\u0061'..'\u007a' |
'\u00c0'..'\u00d6' |
'\u00d8'..'\u00f6' |
'\u00f8'..'\u00ff' |
'\u0100'..'\u1fff' |
'\u3040'..'\u318f' |
'\u3300'..'\u337f' |
'\u3400'..'\u3d2d' |
'\u4e00'..'\u9fff' |
'\uf900'..'\ufaff'
;

TERM : (LETTER|('0'..'9'))+
;

TERM -> คือ String ที่ใช้ในการ search ประกอบด้วย
ตัวอักษร (สังเกตุว่ามี unicode ด้วย) หรือ ตัวเลข
SEMI -> ใส่เพิ่มเข้ามาโดยโปรแกรมเพื่อให้รู้ว่าเป็นตัวปิดท้าย syntax
เป็นตัวปิดท้าย
{$setType(Token.SKIP);}
-> เป็นการบอก lexer ไม่ต้องส่ง token นั้นไปให้ parser

Parser
class AdvQryParser extends Parser;
options {
k = 2;
buildAST = true;
}
search : expression SEMI!
;

primitive : TERM
| LPAREN! expression RPAREN!
;

unaryExpression
: (NEGATE^)?
primitive
;

logicalExpression
: unaryExpression
((AND^ | OR^) unaryExpression
)*
;

expression : logicalExpression
;

พวกที่เป็นตัวใหญ่หมด ก็คือ token ที่ define ไว้ใน lexer
ส่วนเครื่องหมาย "^" หมายถึงว่าต้องการให้ node นั้นเป็น root node
ใน AST ที่ generate ขึ้นมา
เครื่องหมาย "!" ที่ตามหลังก็คือไม่ต้องการให้รวม token นั้นเข้าไปใน AST
ในการเขียน parser เราต้องกำหนด rule ที่เป็นจุดท้ายเข้าหลัก
ของการ parse ในทีนี้ก้คือ "search"

Tree Parser
ผลลัพท์จากการ parse เราจะได้ AST ออกมา
ขั้นต่อไปก็คือการท่อง AST เพื่อที่จะ search Index Repository
class AdvQryTreeParser extends TreeParser;
options {
importVocab=AdvQryParser;
}
{ // java code ที่ customize เพิ่มเข้าไป
// มีวัตถุประสงค์หลักก็เพื่อเชื่อม object ภายนอกเข้ามา
// เพื่อให้สามารถใช้ในขณะที่กำลัง process tree ได้
static final java.util.BitSet EMPTY = new java.util.BitSet();
IndexRepository repo;

public void setIndexRepository(IndexRepository repo) {
this.repo = repo;
}

public java.util.BitSet getByLabel(String labelValue) {
return repo.getQuery().getByLabel(labelValue);
}
}
search
returns [java.util.BitSet ret = EMPTY;]
: ret = expr
;

expr
returns [java.util.BitSet ret = EMPTY;]
: ret = unaryExpr
| ret = biExpr
| ret = single
;

unaryExpr
returns [java.util.BitSet ret = EMPTY;]
:#(NEGATE ret=expr)
{ ret.flip(0, ret.length());}
;

biExpr returns [java.util.BitSet ret = EMPTY;]
{
java.util.BitSet left, right;
}
:#(AND left=expr right=expr) {left.and(right); ret = left;}
|#(OR left=expr right=expr) {left.or(right); ret = left;}
;

single
returns [java.util.BitSet ret = EMPTY;]
:label:TERM { ret = getByLabel(label.getText()); }
;

syntax ของ TreeParser จะเขียนเหมือน lexer กับ parser
คือขึ้นต้นด้วย rule name และตามด้วย Matching Syntax
วิธีการท่อง tree ของเราก็คือทุก node ที่เรา visit จะต้อง
return ผลลัพท์ออกมา ซึ่งผลลัทพ์นั้นก็มีทั้งได้จากการ search repository
หรือผลลัพท์ที่ได้จากการทำ operation AND หรือ operation OR
ประโยค
returns [java.util.BitSet ret = EMPTY;]
ก็คือการ define ว่า rule ของเราจะ return java Class อะไร
และมีค่า default เป็นอะไร
:#(AND left=expr right=expr)
หมายถึงการ matching node ที่มี root node เป็น AND และมี child node อยู่ 2 node
โดยทั้ง 2 node ต้องเป็น expr Node
:#(AND left=expr right=expr) {left.and(right); ret = left;}
กรณี่ที่ maching ได้ก็จะทำ Action ที่ระบุ ในกรณีนี้
ก็คือ call method and ของ object left(Class BitSet)
โดย pass parameter right(class BitSet) ไป
:label:TERM { ret = getByLabel(label.getText()); }
กรณีที่มี node เดียว, Matching Syntax สามารถใส่ตรงๆได้เลย
ไ่ม่ต้องมี #() ให้เกะกะ
ส่วนคำว่า label:TERM มีความหมายว่าให้สร้าง ตัวแปรชื่อ label
ใน code เพื่อที่เราจะใช้อ้างอิงใน action ได้

Integrate Lexer, Parser, TreeParser
หลังจากเรา define Lexer, Parser และ TreeParser
ก็จะต้องทำการ generate java Class ขึ้นมา
ทำโดยสั่งคำสั่ง
java antlr.Tool <inputfile>>

และในส่วน code ของการ search ก็จะเขียนดังนี้
public int[] advSearch(String searchStr) {
AdvQryLexer lexer = new AdvQryLexer(
new StringReader(searchStr));
AdvQryParser parser = new AdvQryParser(lexer);
try {
parser.search();
AdvQryTreeParser tps = new AdvQryTreeParser();
tps.setIndexRepository(repo);
BitSet bs = tps.search(parser.getAST());
return converToArray(bs);

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}


ข้อมูลอ้างอิง

Related link from Roti

Sunday, March 13, 2005

Project Log Analyzer #1

เมือวันศุกร์แวะไปบริษัทฯมา
เจอหนูจิ๋วกำลังหมกมุ่นกับ
การทำเอกสารสรุปรูปแบบการใช้งานของ user
โดยทำการสรุปจาก Application log file
ที่มีขนาดของข้อมูลประมาณ 40000 รายการ ต่อวัน

ตัวอย่างของข้อมูล
2004-10-11 09:13:07,384  ananda 1400 20.17.99.19 /wpm/uc2050.do(method=listWi..
2004-10-11 09:13:19,456 ananda 1400 20.17.99.19 /wpm/uc2050.do(method=search..
2004-10-11 09:13:21,694 ananda 1400 20.17.99.19 /wpm/uc2050.do(method=select..
2004-10-11 09:13:28,390 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=gotoEn..
2004-10-11 09:16:04,091 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=create..
2004-10-11 09:16:07,814 ananda 1400 20.17.99.19 /wpm/uc1040.do(method=print1..

ก็เลยคิดจะทำโปรแกรมช่วยวิเคราะห์ให้
ใช้ idea เรื่อง label จาก Taxonomie ที่คุณ bact note ไว้
ออกแบบให้มี rule ที่ทำหน้าที่ assign label
ให้กับแต่ละบรรทัดใน log file
แล้วผู้ใช้สามารถ query เลือกแสดงผลเฉพาะ
label ที่ต้องการได้

ส่วนแรกที่ต้องทำก็คือส่วน Engine ที่ใช้ในการ assign label ให้กับ log file
โดยออกแบบให้ Engine parse log file ทีละ line
จากนั้นก็ consult rule ว่าควรจะ assign label ให้หรือเปล่า
โดยแบ่ง rule เป็น 2 แบบคือ
  • พวกที่ assign label โดยดึงค่า label มาจากข้อมูลตรงๆ
  • พวกที่ assign fix label โดยดูว่าข้อมุลนั้นมี pattern ตามที่กำหนดหรือไม่
และเพื่อให้ flexible ยิ่งขึ้นก็กำหนดให้มีพวก logic
and, or ใน rule แบบที่ 2 ได้ด้วย
กำหนดให้เก็บข้อมูลการ config rule ในลักษณะ XML File

ตัวอย่าง xml file
<spec>
<tokenizer class="mx.laz.PatternTokenizer" pattern=" ,"/>

<elements>
<element id="user" mapPos="3"/>
<element id="date" mapPos="0"/>
<element id="time" mapPos="1"/>
<element id="action" mapPos="6"/>
</elements>

<labels>
<label id="user" >
<helper
class="mx.laz.helper.CopyFromElement"
elementId="user">
</helper>
</label>

<label id="action" labelValue="uc2110">
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050\.do\(method=selected.*</pattern>
</helper>
</label>

<label id="test" labelValue="2050_ananda">
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050.*</pattern>
</helper>
</left>
<right>
<helper
class="mx.laz.helper.PatternMatch"
elementId="user">
<pattern>ananda</pattern>
</helper>
</right>
</helper>
</label>

<label id="test2" labelValue="2050_ananda_id">
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*uc2050.*</pattern>
</helper>
</left>
<right>
<helper class="mx.laz.helper.AndHelper">
<left>
<helper
class="mx.laz.helper.PatternMatch"
elementId="user">
<pattern>ananda</pattern>
</helper>
</left>
<right>
<helper
class="mx.laz.helper.PatternMatch"
elementId="action">
<pattern>.*id.*</pattern>
</helper>
</right>
</helper>
</right>
</helper>
</label>
</labels>
<indexRepository class="mx.laz.repository.HsqlIndexRepository"/>
</spec>
ลักษณะการทำงาน
  • Engine ทำการอ่าน log file เข้ามาทีละราย
  • tokenizer ทำการ split line ออกเป็น element
  • จากนั้นก็ทำการ assign label ให้กับ line
  • ผลการ assign label จะเก็บไว้ใน indexRepository

การ parse xml file
ปกติเราสามารถแบ่ง tool ที่ใช้ parse xml ออกเป็น 2 พวกใหญ่ๆคือ
พวกที่ 1 ต้องเขียน code เยอะหน่อย ส่วนพวกที่ 2 ก้ต้องมีการเขียน
descriptor บางอย่างที่เป็นตัวกลางระหว่าง java object
กับ xml file เช่น xml schema

Tool ที่เลือกใช้ก็คือ Common Digester
ซึ่งจัดอยู่กึ่งกลางระหว่างพวกที่ 1 กับพวกที่ 2
ที่เลือก Digester ก็เพราะยังไม่เคยใช้
ก็เลยอยากรู้่ว่ามัน powerfull แค่ไหน
Note: subproject ของ jakarta ส่วนใหญ่จะใช้
Digester เป็นตัว parse config file
เช่น Struts ใช้ digester เป็นตัว parse
action mapping file ส่วนใครที่เคยเขียน
server.xml ของ tomcat ผิดๆ คงเคยเห็น stack trace
ของ digester ที่ชวนให้งงว่าเกิดอะไรขึ้น


ตัว Digester จะใช้ SAX event + Stack ในการ parse xml file
เราจะเขียน rule เพื่อ map ให้ Digester รู้ว่าเมื่อเกิด event หนึ่งๆขึ้น
ควรจะทำอะไรดี โดยมี stack เป็นตัวช่วยเก็บข้อมูล (เก็บ temporary object)
ตัวอย่าง event ที่เกิด
  <a>         -- Matches pattern "a"
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
<b> -- Matches pattern "a/b"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
<c/> -- Matches pattern "a/b/c"
</b>
</a>
ตัวอย่าง code ที implement
 
Digester digest = new Digester();
digest.push(this); //push this onto stack

digest.addObjectCreate("spec/tokenizer",
"class", WhiteSpaceTokenizer.class);
digest.addSetProperties("spec/tokenizer");
digest.addSetNext("spec/tokenizer", "setTokenizer");
addObjectCreate คือ rule ที่ใช้บอกว่า
ถ้าเจอ <tokenizer> ที่อยู่ภายใต้ <spec>
ก็ให้ทำการสร้าง Object ขึ้นมาจาก Class ที่กำหนดไว้ใน
Attribute "class" ของ <tokenizer>
โดยมี default class คือ WhiteSpaceTokenizer
เมื่อสร้างเสร็จแล้วก็ให้ push object นั้นไว้ใน stack ด้วย
addSetProperties จะเป็นการ scan หา attribute
ใน current tag แล้วนำไป set ให้กับ object
ที่อยู่บนสุดของ stack
addSetNext เป็นการกำหนดว่าเมื่อจบ
tag tokenizer แล้วให้นำ object ที่อยู่บนสุดของ
stack ออกมา set ให้กับ object บน stack อันถัดไป
โดยใช้ method ชื่อ setTokenizer (ในกรณีนี้ก็ืคือ
this.setTokenizer(XXXTokenizerImpl))
  digest.addObjectCreate("spec/elements/element", 
Element.class);
digest.addSetProperties("spec/elements/element");
digest.addSetNext("spec/elements/element", "addElement");

digest.addObjectCreate("spec/labels/label", Label.class);
digest.addSetProperties("spec/labels/label");
digest.addSetNext("spec/labels/label", "addLabel");
digest.addObjectCreate("spec/labels/label/helper",
"class", DummyHelper.class);
digest.addSetProperties("spec/labels/label/helper");
digest.addBeanPropertySetter(
"spec/labels/label/helper/pattern");
digest.addSetNext("spec/labels/label/helper", "setHelper");

digest.addObjectCreate("spec/indexRepository",
"class", IndexRepository.class);
digest.addSetNext("spec/indexRepository",
"setIndexRepository");
rule addBeanPropertySet คือ rule ที่ใช้ add
ค่า Text Element ที่อยู่ระหว่าง tag pattern ให้กับ property
ที่ชื่อ pattern ของ object ที่อยู่บนสุดของ Stack

ส่วนที่ยากขึ้นมาหน่อยก็คือ ส่วน helper
ที่มีลักษณะ recursive ลงไปได้เรื่อยๆ
digester ก็มี feature ในการ map
SAX Event ในลักษณะของ patern matching เช่นเดียวกัน
  digest.addCallMethod("*/left/helper", 
"setLeft", 1, new Class[]
{AbstractHelper.class});
digest.addCallMethod("*/right/helper",
"setRight", 1, new Class[]
{AbstractHelper.class});

digest.addObjectCreate("*/left/helper", "class", DummyHelper.class);
digest.addObjectCreate("*/right/helper", "class", DummyHelper.class);

digest.addSetProperties("*/left/helper");
digest.addSetProperties("*/right/helper");
digest.addBeanPropertySetter("*/left/helper/pattern");
digest.addBeanPropertySetter("*/right/helper/pattern");

CallParamRule rule = new CallParamRule(0, true);
digest.addRule("*/left/helper", rule);
digest.addRule("*/right/helper", rule);
เครื่องหมาย * แทนการ match อะไรก็ได้
rule addCallMethodเป็นการเตรียม method
object ที่ใช้ call object ที่อยู่บนสุดของ stack
จากนั้นก็ add Method object ไว้ใน stack
rule CallParamRule เป็นการระบุให้
นำเอก object ที่อยู่บนสุดของ stack
ไป set เป็น parameter ให้กับ object
ที่อยู่บนสุดของ stack

Note: สังเกตุว่าเราสามารถเขียน digester rule ได้
2 วิธีคือ digest.addXXX กับ digest.addRule
วิธีแรกเป็นแค่ shortcut ของวิธีที่ 2


Note: จริงแล้วเราสามารถลดรูปในส่วน left กับ right
ได้ โดยกำหนด pattern match เป็น "*/helper/*/helper"
ชึ่งสามารถแทน "*/left/helper" และ "*/right/helper"
แต่ด้วยเหตุผลกดใดไม่ทราบ digester ที่เขียนแบบนี้
จะ match SAX event ไม่เจอ


Note: กรณีที่ xml ซับซ้อนอาจจะต้องมีการ debug ดูว่า digester ทำงานตามที่เราต้องการหรือไม่
ก็ให้add log4j เข้าไปใน class path พร้อมทั้ง set log4j.properties
  log4j.rootLogger=DEBUG, 1
log4j.appender.1=org.apache.log4j.ConsoleAppender
log4j.appender.1.layout=org.apache.log4j.PatternLayout
log4j.appender.1.layout.conversionPattern=%c-%L-%p-%m%n
log4j.logger.org.apache.commons.digester=DEBUG,1



ข้อมูลเพิ่มเติม

Related link from Roti