วันนี้เปิดเจอ site หนึ่งน่าสนใจดี
เป็นที่รวม code snippet
คล้ายๆกับ del.icio.us ที่ assign tag กับ url
อันนี้เปลี่ยนเป็น assign tag กับ code snippet แทน
ตัว application เขียนด้วย ruby on rails
แล้วก็เปิดให้ download ได้ด้วย
Link
Friday, July 08, 2005
Custom Schema Mapping in ActiveRecord (Rails)
ActiveRecord คือ module ที่ทำหน้าที่
Object/relation mapping ใน RubyOnRails Framework
วิธีการที่เขาใช้ จะใช้ reflection เป็นหลัก
และก็มี assumption บางอย่างที่ใช้ในการ
map column name, table name เข้ากับ Object
ตัวอย่างแรกสุดเลย
สมมติเราจะเขียน blog ด้วย Rails
ในแง่ของตัว relational database
เราก็ต้องมี table สำหรับเก็บ blog post
assumption ของ active record ก็คือ
ดังนั้นเจ้า table blog post ของเราก็เลยออกมา
หน้าตาประมาณนี้
ส่วนเจ้า class BlogPost
ก็หน้าตาประมาณนี้
จะเห็นได้ว่าไม่ต้องระบุอะไรเลย
เดี๋ยว Reflection จัดให้เอง ตอน runtime
ส่วนการใช้งาน ก็เช่น
ในกรณีที่เราเขียน app โดยออกแบบใหม่หมด
ตั้งแต่ table เลย ก็คงไม่มีปัญหาอะไร
แต่ถ้าเป็น Web Application ที่ต้องใช้
legacy table จากระบบเก่า
ก็จะเกิดปัญหาเรื่อง naming หรือ mapping ขึ้น
(แค่ชื่อ table ก็เป็นปัญหาแล้ว
บางคนเขานิยมตั้งชื่อ table เป็นรหัส)
ดังนั้นใน post วันนี้ เราจะลองมาดูว่า
เราสามารถ override assumption ต่างๆของ ActiveRecord
ได้อย่างไรบ้าง
เริ่มด้วยชื่อ table
ค้นเจอ 2 แบบ
คืออันแรกใช้วิธี override method table_name()
ส่วนอันที่ 2 ใช้วิธี call method set_table_name("name")
กรณี primary key ของเราไม่ได้ชื่อ "id"
ส่วนกรณีที่เราต้องการชื่อ attribute
ที่ไม่ตรงกับชื่อ column
เท่าที่ค้นดู ยังไม่มีพูดถึงประเด็นนี้
แต่ถ้าทำด้วยวิธี object style ก็คงใช้วิธีเขียน method set, get ด้วยชื่อใหม่
แล้วข้างไนก็ค่อยไปเรียก set, get ด้วยชื่อจริง
Object/relation mapping ใน RubyOnRails Framework
วิธีการที่เขาใช้ จะใช้ reflection เป็นหลัก
และก็มี assumption บางอย่างที่ใช้ในการ
map column name, table name เข้ากับ Object
ตัวอย่างแรกสุดเลย
สมมติเราจะเขียน blog ด้วย Rails
ในแง่ของตัว relational database
เราก็ต้องมี table สำหรับเก็บ blog post
assumption ของ active record ก็คือ
- ต้องมี column "id" เป็น primary key
- ถ้ามี column created_on กับ updated_on
เจ้า ActiveRecord จะดูแลการ set ค่าพวกนี้ให้โดยอัตโนมัติ - ชื่อ table ต้องอยู่ในรูปพหูพจน์
ดังนั้นเจ้า table blog post ของเราก็เลยออกมา
หน้าตาประมาณนี้
create table blog_posts (
id serial,
post text,
created_on timestamp,
primary key (id)
);
ส่วนเจ้า class BlogPost
ก็หน้าตาประมาณนี้
class BlogPost < ActiveRecord::Base
end
จะเห็นได้ว่าไม่ต้องระบุอะไรเลย
เดี๋ยว Reflection จัดให้เอง ตอน runtime
ส่วนการใช้งาน ก็เช่น
blogpost = BlogPost.new
blogpost.post = "hello world"
blogpost.save
ในกรณีที่เราเขียน app โดยออกแบบใหม่หมด
ตั้งแต่ table เลย ก็คงไม่มีปัญหาอะไร
แต่ถ้าเป็น Web Application ที่ต้องใช้
legacy table จากระบบเก่า
ก็จะเกิดปัญหาเรื่อง naming หรือ mapping ขึ้น
(แค่ชื่อ table ก็เป็นปัญหาแล้ว
บางคนเขานิยมตั้งชื่อ table เป็นรหัส)
ดังนั้นใน post วันนี้ เราจะลองมาดูว่า
เราสามารถ override assumption ต่างๆของ ActiveRecord
ได้อย่างไรบ้าง
เริ่มด้วยชื่อ table
ค้นเจอ 2 แบบ
class MyClassName < ActiveRecord::Base
def self.table_name() "my_table_name" end
end
class Mouse < ActiveRecord::Base
set_table_name "mice"
end
คืออันแรกใช้วิธี override method table_name()
ส่วนอันที่ 2 ใช้วิธี call method set_table_name("name")
กรณี primary key ของเราไม่ได้ชื่อ "id"
class MyClass < ActiveRecord::Base
set_primary_key "my_pk_name"
end
ส่วนกรณีที่เราต้องการชื่อ attribute
ที่ไม่ตรงกับชื่อ column
เท่าที่ค้นดู ยังไม่มีพูดถึงประเด็นนี้
แต่ถ้าทำด้วยวิธี object style ก็คงใช้วิธีเขียน method set, get ด้วยชื่อใหม่
แล้วข้างไนก็ค่อยไปเรียก set, get ด้วยชื่อจริง
class Song < ActiveRecord::Base
def length=(minutes)
write_attribute(:len, minutes)
end
def length
read_attribute(:len)
end
end
Related link from Roti
Thursday, July 07, 2005
Syntax Highligh Java source in Apache Forrest
เป้าหมายที่ต้องการทำก็คือ
ต้องการให้ Apache Forrest Application สามารถ
transform xdoc ในส่วนของ java source
ให้อยู่ในรูปสีสรรสดสวย
(เดิมใน xdoc มี tag ที่ชื่อ source
อยู่แล้ว แต่ไม่ได้ทำ highlight ให้)
วิธีการที่ทำได้มีอยู่ 2 ทางหลักๆ
วิธีแรกดูดีสุด
โดยในคู่มือ 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 ไว้ดังนี้
นั่นคือหลังจากได้ url ที่ลงท้ายด้วย .html เข้ามา
เจ้า forrest ก็จะแตกออกเป็น 5 dispatch ย่อย ที่จะ
แจกไปให้แต่ละส่วนทำงาน เช่น ส่วน tab, ส่วน menu
จากนั้นค่อย aggregate รวมกันแล้วส่งไป render อีกที
กรณีของเรา ก็คือต้องการ customize ส่วน body
จากของเดิมที่เป็นดังนี้
โดยสิ่งที่เราจะเพิ่มเข้าไปก็คือ หลังจากที่ generator ได้ file xdoc
มาแล้ว เราจะเข้าไป intercept sax event
เพื่อที่ว่าจะได้ทำการ parse java source ให้อยู่ในรูป
xml เสียก่อน จากนั้นก็จะส่งผ่านไปให้ xslt
ทำการ render ให้อยู่ในรูปแบบที่ต้องการ
ตัว transform type "java2html" จะทำหน้าที่แปลง
javasource ให้อยู่ในรูป xml โดยมี tag แปะตาม
ประเภทของ token
เช่น
ก็จะถูกแปลงเป็น
ส่วน transform อีกตัวจะทำหน้าที่ apply style sheet "javahighlight.xsl"
เพื่อ render ให้อยู่ในรูปแบบ html ที่ต้องการ
วิธีการ implement transformer
เขียน class Java2HtmlTransformer โดย extends AbstractSaxTransformer
หลังจากเขียน class แล้ว เราต้องบอกให้ Forrest
รู้จัก transformer นี้โดยการเพิ่ม config เข้าไปใน sitemap.xmap
ลักษณะการทำงานของ Java2HtmlTransformer ก็คือเราจะสนใจ sax event
เฉพาะส่วน startElement กับ endElement
กรณีที่พบ tag <javasource> เราก็จะทำการ
record text ที่เกิดขึ้นระหว่าง tag เปิด กับ tag ปิด
และทำการส่งไปให้ JavaXmlRenderer ทำการ parse text ในส่วนนี้
ส่วน Class JavaXmlRenderer ใช้ source code ตัวอย่างจาก
project JHighlight
โดยหลักการทำงาน ก็คือ JavaXmlRenderer จะเรียกใช้ scanner ที่ชื่อ
JavaHighlighter (implement ด้วย Flex)
ทำการ scan ทีละบรรทัด
ผลลัพท์ที่ได้ จะได้ style กับ token ออกมา
จากนั้น ก็จะทำการส่ง sax event ออกไป
โดยมี tag name ตามรูปแบบ style ที่ได้มาจาก scanner
ตัวอย่างการนำไปใช้
ผลลัพท์ที่ได้
สรุป ปัญหาของการ customize forrest หรือ cocoon framework ก็คือ
เกิดจากเจ้าตัว cocoon framework ที่ค่อนข้างจะซับซ้อนพอสมควร
แถมมันยังใช้ IOC container ที่ชื่อ Avalon Excalibur
ยิ่งทำให้การทำความเข้าใจ source code ต่างๆ ต้องใช้เวลามาก
นอกจากนี้ ยังมีประเด็นความเข้าใจในส่วนของการ parse xml Document
ที่มักจะมีปัญหาเรื่อง namespace เสมอ
ต้องการให้ 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 แบบคือ
พยายามจะทำแบบที่ 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
Tuesday, July 05, 2005
Sunday, July 03, 2005
ยืนด้วยครับ
เมื่อวานออกไปทำธุระนอกบ้้าน
เดินไปถึงหน้าปากซอย
พบตำรวจกำลังปิดถนน
รอขบวนเสด็จ
ก็เลยขี้นิ้วไปที่สะพานลอย
(ทำนองว่า ข้ามได้ไหม)
ตำรวจก็บอก เดี๋ยวก่อน
ก็เลยไปนั่งรอข้างๆสะพานลอย
(เปิดอิฐก่อสูงประมาณ ฟุตครึ่ง)
ตำรวจมอง แล้วก็บอกว่า "ช่วยยืนด้วยครับ"
รู้สึกจี๊ดขึ้นมาที่หัวเลย
แต่ก็ยังสงบอาการ
บอกตำรวจไปว่า
"เดี๋ยวมาแล้วค่อยยืน"
พอขบวนมาถึง
ตำรวจทำท่ายืน ระเบียบพัก
แล้วก็พูดอีก "ยืนด้วยครับ"
คราวนี้ ก็เลยส่ายหัว แล้วก็บอกว่า "ไม่ยืน"
(นึกในใจ ทำไมกูต้องยืนด้วยวะ)
จบ
ปล. เจอเหตุการณ์นี้แล้วนึกถึง
ตอนไปเที่ยวพระที่นั่งวิมาณเมฆ
สวยดีนะ เสียอย่างเดียว พอ guide (พวกราชการ)
นำชมมาถึงห้องสุดท้าย
guide ก็บอกว่า ทุกคนก้มลงกราบพระรูปพร้อมกันด้วยค่ะ
แถมมีบทพูดถวายบังคม ที่ต้องพูดพร้อมกันด้วยนะ
ปล. ในเชิงวัฒนธรรมแล้ว
ถือได้ว่าเป็นเรื่องเดียวกับ
เรื่องรุ่นน้องต้องเคารพรุ่นพี่
(หรือรุ่นพี่สั่งยืนแถว)
ในตอนรับน้องไหมเนี่ย
เดินไปถึงหน้าปากซอย
พบตำรวจกำลังปิดถนน
รอขบวนเสด็จ
ก็เลยขี้นิ้วไปที่สะพานลอย
(ทำนองว่า ข้ามได้ไหม)
ตำรวจก็บอก เดี๋ยวก่อน
ก็เลยไปนั่งรอข้างๆสะพานลอย
(เปิดอิฐก่อสูงประมาณ ฟุตครึ่ง)
ตำรวจมอง แล้วก็บอกว่า "ช่วยยืนด้วยครับ"
รู้สึกจี๊ดขึ้นมาที่หัวเลย
แต่ก็ยังสงบอาการ
บอกตำรวจไปว่า
"เดี๋ยวมาแล้วค่อยยืน"
พอขบวนมาถึง
ตำรวจทำท่ายืน ระเบียบพัก
แล้วก็พูดอีก "ยืนด้วยครับ"
คราวนี้ ก็เลยส่ายหัว แล้วก็บอกว่า "ไม่ยืน"
(นึกในใจ ทำไมกูต้องยืนด้วยวะ)
จบ
ปล. เจอเหตุการณ์นี้แล้วนึกถึง
ตอนไปเที่ยวพระที่นั่งวิมาณเมฆ
สวยดีนะ เสียอย่างเดียว พอ guide (พวกราชการ)
นำชมมาถึงห้องสุดท้าย
guide ก็บอกว่า ทุกคนก้มลงกราบพระรูปพร้อมกันด้วยค่ะ
แถมมีบทพูดถวายบังคม ที่ต้องพูดพร้อมกันด้วยนะ
ปล. ในเชิงวัฒนธรรมแล้ว
ถือได้ว่าเป็นเรื่องเดียวกับ
เรื่องรุ่นน้องต้องเคารพรุ่นพี่
(หรือรุ่นพี่สั่งยืนแถว)
ในตอนรับน้องไหมเนี่ย
Related link from Roti
Subscribe to:
Posts (Atom)