Thursday, July 07, 2005

Syntax Highligh Java source in Apache Forrest

เป้าหมายที่ต้องการทำก็คือ
ต้องการให้ Apache Forrest Application สามารถ
transform xdoc ในส่วนของ java source
ให้อยู่ในรูปสีสรรสดสวย
(เดิมใน xdoc มี tag ที่ชื่อ source
อยู่แล้ว แต่ไม่ได้ทำ highlight ให้)

วิธีการที่ทำได้มีอยู่ 2 ทางหลักๆ
  • Customize Project 's Pipeline
  • Customize Forrest 's Pipeline ที่ forrest installation directory โดยตรง


วิธีแรกดูดีสุด
โดยในคู่มือ forrest บอกไว้ว่า เราสามารถ
customize pipeline processing เฉพาะใน Document Project ของเรา
ผ่านทาง /src/documentation/sitemap.xmap ได้
โดยทำได้ 2 แบบคือ
  • render เองหมดเลย
  • render เฉพาะ body ที่เหลือปล่อยให้ forrest apply skin ให้

  • พยายามจะทำแบบที่ 2 แต่ไม่ได้สักที
    ก็เลยเปลี่ยนใจ ย้ายไป customize ที่ตัว
    forrest configuration โดยตรงเลย

    เริ่มจาก file /$FORREST_HOME/main/webapp/sitemap.xmap
    มันจะมีการ mapping url ไว้ดังนี้
    <map:match pattern="*.html">
    <map:aggregate element="site">
    <map:part src="cocoon:/skinconf.xml"/>
    <map:part src="cocoon:/build-info"/>
    <map:part src="cocoon:/tab-{0}"/>
    <map:part src="cocoon:/menu-{0}"/>
    <map:part src="cocoon:/body-{0}"/>
    </map:aggregate>

    <map:call resource="skinit">
    <map:parameter name="type" value="site2xhtml"/>
    <map:parameter name="path" value="{0}"/>
    </map:call>
    </map:match>


    นั่นคือหลังจากได้ url ที่ลงท้ายด้วย .html เข้ามา
    เจ้า forrest ก็จะแตกออกเป็น 5 dispatch ย่อย ที่จะ
    แจกไปให้แต่ละส่วนทำงาน เช่น ส่วน tab, ส่วน menu
    จากนั้นค่อย aggregate รวมกันแล้วส่งไป render อีกที

    กรณีของเรา ก็คือต้องการ customize ส่วน body
    จากของเดิมที่เป็นดังนี้
    <map:match pattern="**body-*.html">
    <map:generate src="cocoon:/{1}{2}.xml"/>
    <map:transform type="idgen"/>
    <map:transform type="xinclude"/>
    <map:transform type="linkrewriter" src="cocoon:/{1}linkmap-{2}.html"/>
    <map:transform src="resources/stylesheets/declare-broken-site-links.xsl" />
    <map:call resource="skinit">
    <map:parameter name="type" value="document2html"/>
    <map:parameter name="path" value="{1}{2}.html"/>
    <map:parameter name="notoc" value="false"/>
    </map:call>
    </map:match>


    โดยสิ่งที่เราจะเพิ่มเข้าไปก็คือ หลังจากที่ generator ได้ file xdoc
    มาแล้ว เราจะเข้าไป intercept sax event
    เพื่อที่ว่าจะได้ทำการ parse java source ให้อยู่ในรูป
    xml เสียก่อน จากนั้นก็จะส่งผ่านไปให้ xslt
    ทำการ render ให้อยู่ในรูปแบบที่ต้องการ

    <map:match pattern="**body-*.html">
    <map:generate src="cocoon:/{1}{2}.xml"/>
    <!-- parse java source to xml form -->
    <map:transform type="java2html" />
    <map:transform type="idgen"/>
    <map:transform type="xinclude"/>
    <map:transform type="linkrewriter" src="cocoon:/{1}linkmap-{2}.html"/>
    <!-- render xml syntax tree to html tag -->
    <map:transform src="resources/stylesheets/javahighlight.xsl"/>
    <map:transform src="resources/stylesheets/declare-broken-site-links.xsl" />
    <map:call resource="skinit">
    <map:parameter name="type" value="document2html"/>
    <map:parameter name="path" value="{1}{2}.html"/>
    <map:parameter name="notoc" value="false"/>
    </map:call>
    </map:match>


    ตัว transform type "java2html" จะทำหน้าที่แปลง
    javasource ให้อยู่ในรูป xml โดยมี tag แปะตาม
    ประเภทของ token
    เช่น
    package pok.test;
    import java.util.ArrayList;

    ก็จะถูกแปลงเป็น
    <javakeyword>package</javakeyword pok.test;
    <javakeyword>import</javakeyword> java.util.<java_type>ArrayList</java_type>;


    ส่วน transform อีกตัวจะทำหน้าที่ apply style sheet "javahighlight.xsl"
    เพื่อ render ให้อยู่ในรูปแบบ html ที่ต้องการ
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template match="/">
    <xsl:apply-templates/>
    </xsl:template>

    <!-- the obligatory copy-everything -->
    <xsl:template match="node() | @*">
    <xsl:copy>
    <xsl:apply-templates select="@*"/>
    <xsl:apply-templates/>
    </xsl:copy>
    </xsl:template>

    <xsl:template match="jshl">
    <pre class="hl">
    <xsl:apply-templates mode="javasource"/>
    </pre>
    </xsl:template>

    <xsl:template match="java_plain" mode="javasource">
    <xsl:apply-templates/>
    </xsl:template>

    <xsl:template match="java_keyword" mode="javasource">
    <span class="S_KEYWORD1"><xsl:apply-templates/></span>
    </xsl:template>
    ...


    วิธีการ implement transformer
    เขียน class Java2HtmlTransformer โดย extends AbstractSaxTransformer
    package pok.cocoon.transformer;



    import java.io.StringBufferInputStream;

    import org.apache.cocoon.transformation.AbstractSAXTransformer;
    import org.apache.cocoon.xml.AttributesImpl;
    import org.xml.sax.Attributes;
    import org.xml.sax.SAXException;



    public class Java2HtmlTransformer extends AbstractSAXTransformer {

    private static final String TAG_NAME = "javasource";

    public void endElement(String uri, String name, String raw)
    throws SAXException {
    if (name.equals(TAG_NAME)) {



    String srcStr = this.endTextRecording();
    JavaXmlRenderer render = new JavaXmlRenderer();
    sendStartElementEvent("jshl");
    try {

    render.highlight(new StringBufferInputStream(srcStr), this);

    } catch (Throwable e) {
    e.printStackTrace();
    }
    sendEndElementEvent("jshl");

    } else {
    super.endElement(uri, name, raw);
    }

    }

    public void startElement(String uri, String name, String raw,
    Attributes attr) throws SAXException {

    if (name.equals(TAG_NAME)) {
    this.startTextRecording();
    } else {

    super.startElement(uri, name, raw, attr);
    }
    }

    }

    หลังจากเขียน class แล้ว เราต้องบอกให้ Forrest
    รู้จัก transformer นี้โดยการเพิ่ม config เข้าไปใน sitemap.xmap
    <map:transformers>
    <map:transformer name="java2html"
    src="pok.cocoon.transformer.Java2HtmlTransformer"
    logger="pok"
    />
    </map:transformers>


    ลักษณะการทำงานของ Java2HtmlTransformer ก็คือเราจะสนใจ sax event
    เฉพาะส่วน startElement กับ endElement
    กรณีที่พบ tag <javasource> เราก็จะทำการ
    record text ที่เกิดขึ้นระหว่าง tag เปิด กับ tag ปิด
    และทำการส่งไปให้ JavaXmlRenderer ทำการ parse text ในส่วนนี้

    ส่วน Class JavaXmlRenderer ใช้ source code ตัวอย่างจาก
    project JHighlight
    package pok.cocoon.transformer;

    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.Reader;
    import java.io.StringReader;

    import org.apache.cocoon.transformation.AbstractSAXTransformer;

    import com.uwyn.jhighlight.highlighter.JavaHighlighter;
    import com.uwyn.jhighlight.tools.StringUtils;

    public class JavaXmlRenderer {



    public void highlight(InputStream in, AbstractSAXTransformer sax) throws Exc
    eption {

    JavaHighlighter highlighter = new JavaHighlighter();
    highlighter.ASSERT_IS_KEYWORD = true;

    Reader isr;
    isr = new InputStreamReader(in);


    BufferedReader r = new BufferedReader(isr);


    String line;
    String token;
    int length;
    int style;
    String css_class;
    String previous_class = null;
    int previous_style = 0;
    while ((line = r.readLine()) != null)
    {
    line += "\n";
    line = StringUtils.convertTabsToSpaces(line, 4);

    // should be optimized by reusing a custom LineReader class
    Reader lineReader = new StringReader(line);
    highlighter.setReader(lineReader);
    int index = 0;
    while (index < line.length())
    {
    style = highlighter.getNextToken();
    length = highlighter.getTokenLength();
    token = line.substring(index, index + length);

    if (style != previous_style)
    {
    css_class = getCssClass(style);

    if (css_class != null)
    {
    if (previous_class != null)
    {
    sax.sendEndElementEvent(previous_class);
    }
    sax.sendStartElementEvent(css_class);

    previous_class = css_class;
    previous_style = style;
    }
    }
    sax.sendTextEvent(token);

    index += length;
    }

    }
    }


    protected String getCssClass(int style)
    {
    switch (style)
    {
    case JavaHighlighter.PLAIN_STYLE:
    return "java_plain";
    case JavaHighlighter.KEYWORD_STYLE:
    return "java_keyword";
    case JavaHighlighter.TYPE_STYLE:
    return "java_type";
    case JavaHighlighter.OPERATOR_STYLE:
    return "java_operator";
    case JavaHighlighter.SEPARATOR_STYLE:
    return "java_separator";
    case JavaHighlighter.LITERAL_STYLE:
    return "java_literal";
    case JavaHighlighter.JAVA_COMMENT_STYLE:
    return "java_comment";
    case JavaHighlighter.JAVADOC_COMMENT_STYLE:
    return "java_javadoc_comment";
    case JavaHighlighter.JAVADOC_TAG_STYLE:
    return "java_javadoc_tag";
    }
    return null;
    }

    }


    โดยหลักการทำงาน ก็คือ JavaXmlRenderer จะเรียกใช้ scanner ที่ชื่อ
    JavaHighlighter (implement ด้วย Flex)
    ทำการ scan ทีละบรรทัด
    ผลลัพท์ที่ได้ จะได้ style กับ token ออกมา
    จากนั้น ก็จะทำการส่ง sax event ออกไป
    โดยมี tag name ตามรูปแบบ style ที่ได้มาจาก scanner

    ตัวอย่างการนำไปใช้
    <javasource>
    package pok.test;
    import java.util.ArrayList;

    /**
    * this is <code>test</code>
    * @param hi
    */
    public class Main implements java.io.Serializable {
    public static void main(String[] args) {
    System.out.println("helloWorld");
    if (1 != 2) {
    if (1 > 3) {
    // pok test
    }
    }
    }
    }
    </javasource>

    ผลลัพท์ที่ได้


    สรุป ปัญหาของการ customize forrest หรือ cocoon framework ก็คือ
    เกิดจากเจ้าตัว cocoon framework ที่ค่อนข้างจะซับซ้อนพอสมควร
    แถมมันยังใช้ IOC container ที่ชื่อ Avalon Excalibur
    ยิ่งทำให้การทำความเข้าใจ source code ต่างๆ ต้องใช้เวลามาก
    นอกจากนี้ ยังมีประเด็นความเข้าใจในส่วนของการ parse xml Document
    ที่มักจะมีปัญหาเรื่อง namespace เสมอ

    Related link from Roti

    No comments: