分享
 
 
 

Tapestry入门

王朝java/jsp·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

简介

Jakarta Tapestry是一个开源的Java Web应用框架。你或许会说:"大同小异的东西罢了."多数

情况下,我同意你的观点,然而,只要花上些许时间研究一下,你会发现Tapestry跟别的框架大为不同,它是值得严肃对待的。

Tapestry是一个基于控件的框架以致于用它开发Web应用类似开发传统的GUI应用。你用Tapestry开发Web应用时你无需关注以操作为中心的(operation-centric) Servlet API.引用Tapestry网站上的一句话:"Tapestry用对象(objects),方法(methods),属性(properties)替代以往的URLs和查询参数, 重新诠释Web应用开发.Tapestry 3.0即将发布,它有大量的改进和新的特性。

Tapestry的目标

简单性

Tapestry应用与传统的Servlet应用相比代码量更少。大多数传统的Servlet应用包含如下厌烦和

重复的任务:解析查询参数,处理HttpSession对象,构建URLs。Tapestry消除了传统Servelt应用中许多无趣的"衔接"代码("plumbing" code)使开发者把精力集中到应用逻辑上来。

一致性

Tapestry为开发Web应用的页面提供了一致的方式。这样有助于消除传统Servlet应用开发中臆测。由于所有Tapestry应用中的页面都是用相同的可复用的控件组织而成,所以工作方式是相似的。

效率

Tapestr应用拥有高度的可升级性,它利用缓存和对象池使每个请求的处理时间最小化。Tapestry应用拥有跟传统Servlet应用相仿的性能。

错误反馈

任何开发过Servlet/JSP应用的人毫无质疑有类似经历:为了找出Web.xml文件有什么错误,不得不花费大量时间察看浏览器中的堆栈信息。Tapestry拥有优秀的错误报告方式,最值得一提的是它会指出哪个文件以及那一行导致了错误。

与Struts比较

既然Apache Struts可能是当今应用最广泛的Web应用框架,拿Tapestry与它比较是唯一公平的。以下是一些观察比较,它们来源于为这篇文章开发的几个简单的Tapestry应用和为几个Struts工程的工作经历。

Struts的优点

1 一个Servlet/JSP开发者熟悉Struts无需太久。然而Tapestry的学习曲线会长一点,因为它与流行的Web应用框架不太相同 。

2 Struts在Java社区里被广泛接受和使用。为你的项目找一个好的Struts开发人员并非难事,Tapestry近来在开发者社区里 赢得一些关注,然而仍有许多Java老手不知道Tapestry为何物。

3 既然Struts被如此广泛的使用,所以有很多Struts资源可供参考。相比大多数开源软件,Tapestry拥有非常可观的资源和文档,但跟Struts相比仍有差距。

Tapestry的优点

1 你开发一个Tapestry应用无需关注Servlet API,你也无须为你的Servlet应用写一些典型的"衔接"代码。虽然Struts简化了工作,然而Servlet API 仍是你需面对的。

2 Tapestry的页面模板除了几个特别属性和标识就是一个标准的HTML文件。 如果你是一个开发小组中的HTML设计高手,然而你不懂Java或JSP,这就给你带来很大方便。

3 因为Tapestry页面是标准的HTML,所以可以用HTML所见即所得(WYSIWYG)编辑器编辑和预览该页。当一个页需要修改它的外观并不需要通知服务器让它重新编译JSP.

4 Tapestry不需要一个至高的,整个应用范围的配置文件。Tapestry应用中的每一页是独立的,改变一页不会影响开发其他 页面的开发者,因为并没有一个配置文件把所有页面的浏览粘连在一起。

5 Tapestry拥有极好的错误报告。如果你在一个页的模板或页面规范犯了一个错误,Tapestry会指出导致错误的行号。

6 用Tapestry开发是种乐趣。这样说听上去似乎老调,然而用Tapestry开发一个Web应用相比应用其他流行的框架更为自然有趣。用Tapestry开发是应用了一种基于控件的架构,与开发传统的GUI应用非常相似。

Tapestry 架构

Tapestry框架是标准Servlet API的一种扩展。它需要J2SDK1.2或更高版本的J2SDK和一个与Servlet API 2.2(或更高)兼容的应用服务器/Servlet容器。

一个Tapestry应用由许多拥有唯一名称的页面组成。一个页面由一个模板和一些可复用的控件构成。模板由标准的HTML标签和一些额外的属性和标签构成,这些额外的属性和标签是为了告诉Tapestry框架这个页面的那些部分是由Tapestry控件组成。

简单的Tapestry应用

为了最好的描述构建一个Tapestry页面的方方面面,我们可以看看这个Pig Latin翻译器应用的代码。这个应用只有一个页面,在这页里输入一个text值把它翻译成Pig Latin,然后把翻译好的值显示给用户。

在Tapestry应用中每个页由3个部分组成:一个HTML模板,一个页面规范文件,一个Java类。

这里有这个页面屏幕抓图:

页面模板由标准的HTML标签和一些额外的属性和标签构成,这些额外的属性和标签是为了告诉Tapestry框架这个页面的那些部分是由Tapestry控件组成。页面模板存放在Web应用的根context目录下。通常,Tapestry在启动时会寻找和呈现一个名叫"Home"的页。虽然我们可以改变这种行为,但依照Tapestry的惯例会更简单。

Home.html

<html>

<head>

<title>Tapestry Pig Latin Translator</title>

</head>

<body>

<h1>Pig Latin Translator</h1>

<form jwcid="@Form"① listener="ognl:listeners.submit"②>

<table border="1">

<tr>

<td>Value to Translate:</td>

<td>

<input type="text" jwcid="@TextField"③ value="ognl:inputValue"/>

</td>

</tr>

<tr>

<td>Pig Latin:</td>

<td>

<jwcid="@Insert"④ value="ognl:pigLatinValue"/>

</td>

</tr>

</table>

<input type="submit" jwcid="@Submit"⑤ value="Translate"/>

</form>

</body>

</html>

页面模板的绝大部分是普通的HTML,只有少部分Tapestry特有的属性和标签。这种模板机制的优势就是Tapestry页面模板可以在一个可见即所得的编辑器里创建和预览。描述Tapestry控件部分的标识是有限的和突出的。

标识里的jwcid所指是被应用的Tapestry控件的Java Web Component ID.在上面代码断里,我们是隐式地使用控件。隐式的控件是指直接在页面模板里声明使用的控件,。jwcid的前缀 '@ '符号就是通知Tapestry这里声明使用了一个隐式控件。

在上面的Pig Latin Translator页面模板里用了四个控件:Form①, TextField③, Insert④ 和Submit⑤。它们只是Tapestry框架提供的包含超过40个控件的控件库里的四个。在后面的范例中,我们将会看到如何使用显式控件。显式控件是指控件在页面规范文件里声明后再使用的控件。

在前面的HTML模板里,使用控件的同时,也为控件指定了参数。例如控件Form①有一个listener②参数它指定了当表单提交时对应的页面类调用的方法名称。那个ognl:前缀的使用贯穿页面的HTML模板,指向的是Object Graph Navigation Language (OGNL)。OGNL是一个强大的开源的表达式语言,用于将页面内控件的属性绑定到页面类的属性。

现在我们看看页面规范文件。页面规范文件是一个扩展名为page的XML文件,这个文件有许多职责,在众多职责中最基本是指定页面对应的Java类。页面规范文件存放在webapp的WEB_INF目录。

Home.page

<?xml version="1.0"?>

<!DOCTYPE page-specification PUBLIC

"-//Apache Software Foundation//Tapestry Specification 3.0//EN"

"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd>

<page-specification class="Home">

<property-specification name="inputValue" type="java.lang.String"/>

<property-specification name="pigLatinValue" type="java.lang.String"/>

</page-specification>

页面规范文件的根元素有一个class的属性,它指定了这个页对应的Java类。这个类必须要实现org.apache.tapestry.Ipage接口。页面规范同时页定义了两个属性(property)元素,以便Tapestry在页面类里创建新的属性。

Tapestry框架提供了org.apache.tapestry.html.BasePage class,它实现了Ipage接口。页面类被存放在Web-INF/classes目录下,跟你的Web应用的所需要的其他类放在一起。

Home.java

import org.apache.tapestry.html.BasePage;

import org.apache.tapestry.IRequestCycle;

public abstract class Home extends BasePage {

public abstract String getInputValue();

public abstract void setInputValue(String inputValue);

public abstract String getPigLatinValue();

public abstract void setPigLatinValue(String pigLatinValue);

public void submit(IRequestCycle cycle) {

String inputValue = getInputValue();

String pigLatinValue = new PigLatinTranslator().translate(inputValue);

setPigLatinValue(pigLatinValue);

}

}

你要提醒的第一件事或许是这个类为什么是抽象类。它还有几个抽象方法访问inputValue,pigLatinValue属性。这里利用了Tapestry会在运行时刻创建子类的功能,这个子类会创建你在页面规范里声明的属性和生成相应的访问方法。

在表单提交时页面类的submit方法会被调用。为什么会这样?因为我们在页面模板里将Form控件的listener属性指定为:ognl:listeners.submit。这就意味着一个名叫submit的listener会通过页面类的listeners被访问。

所有的页面类和控件类都从org.apache.tapestry.AbstractComponent这个类继承来一个叫listeners的属性。当submit方法完成后,页面会显示被翻译好的词。

最后讲讲Web.xml这个Web发布描述文件。Tapestry,像许多其他的流行的Web应用框架一样,由一个Servlet构成,但是还需要一个发布描述文件。那个发布描述文件应该被存放在WEB-INF目录。

web.xml

<?xml version="1.0"?>

<!DOCTYPE web-app PUBLIC

"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

"http://java.sun.com/dtd/web-app_2_3.dtd>

<web-app>

<display-name>Tapestry Pig Latin Translator</display-name>

<servlet>

<servlet-name>tapestry</servlet-name>

<servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>tapestry</servlet-name>

<url-pattern>/app</url-pattern>

</servlet-mapping>

</web-app>

虽然Pig Latin翻译应用非常简单,但是它会让你对在Tapestry应用中一个页面的3个组成部分有了基本的了解。它也展示了创建一个Tapestry应用的一个页面只需写多么少的代码。

表单输入验证

Tapestry 提供了一些控件以便校验用户的输入。校验子系统是ValidField控件的核心。在下面的登录应用中我们将使用ValidField控件。ValidField控件位于表单内,对用户在客户端的校验提供了有用的反馈和视觉上的错误提示。

区域化

在Tapestry中,区域化是相当简单的。Tapestry允许文字和图形的区域化。为了区域化页面的内容,你可以为每一个添加一个properties文件,或者提供一个区域化的模板。为每个页面提供一个资源文件的方式远比管理和维护一个巨大的全局的应用范围的资源文件简单。如果页面的区域化并不仅仅只是文字信息的区域化,例如页面的布局不同或者组成的控件不同,这样情况提供区域化的页面模板就能派上用场了。我们会在下面的登录应用的使用Tapestry区域化。

创建控件

Tapestry发布时提供了40多个自带的控件。如果你想知道更多的关于Tapestry自带控件的信息,请参考Tapestry Component Reference.想看看Tapestry的控件的应用范例可访问Tapestry Component Workbench.如果你发现你需要一个Tapestry本身没提供的控件,你可以自己创建一个。创建你自己的Tapestry控件跟创建一个页面是相似的。一个典型的Tapestry控件由一个控件规范文件(XML文档),一个HTML控件模板,一个实现了org.apache.tapestry.Icomponent接口的Java类。这个议题有点超出本文的范围,但是如果你有兴趣学习如何创建你自己的Tapestry控件,你可以参考Tapestry的原创人,Tapestry In Action 一书的作者--Howard Lewis Ship写的 Designing Tapestry Mega-Components 。

Tapestry 登录应用

你在Pig latin翻译器应用中看到了Tapestry的一些基本特性。与其用一个复杂的应用展示Tapestry所有的特性以致于压得你揣不过气来,还不如通过一些简单的应用让你找到一点对Tapestry的感觉。下面这个应用展示Tapestry如何处理页面导航,区域化,验证和其他一些特性。

这里有一个Home页的屏幕抓图,下面跟着它的页面模板。

Home.html

<html>

<head>

<title>Welcome to the Tapestry Login Application</title>

</head>

<body>

<h1>Welcome to the Tapestry Login Application</h1>

<span jwcid="@PageLink"① page="Login">Login</span>

</body>

</html>

这个Home页的页面模板除了一个jwcid属性定义使用一个Tapestry PageLink①控件以外都是标准的HTML。

PageLink控件生成了一个指向Login页的超链接。既然Home页没有任何动态的行为所以它不需要页面规范和页面对应的Java类。

这里是Login页的屏幕抓图,后面跟着是它的页面模板。

Login.html

<html>

<head>

<title>

<span key="title">①Login</span>

</title>

</head>

<body jwcid="@Body">②

<span jwcid="@Conditional" condition="ognl:beans.delegate.hasErrors">③

<div style="color: red">

<span jwcid="@Delegator" delegate="ognl:beans.delegate.firstError">④

Error Message

</span>

</div>

</span>

<p style="font-weight: bold" >

<span key="hint">Hint: Your password is your username spelled backwards.</span>

</p>

<form jwcid="@Form" listener="ognl:listeners.login" delegate="ognl:beans.delegate">

<table>

<tr>

<td align="right">

<span jwcid="@FieldLabel" field="ognl:components.inputUsername"⑥>

Username:

</span>

</td>

<td>

<input type="text" jwcid="inputUsername"⑦ value="simpson_h"

size="30"/>

</td>

</tr>

<tr>

<td align="right">

<span jwcid="@FieldLabel" field="ognl:components.inputPassword">

Password:

</span>

</td>

<td>

<input type="text" jwcid="inputPassword" hidden="true" value=""

size="30"/>

</td>

</tr>

<tr>

<td colspan="2" align="center">

<input type="submit" jwcid="@Submit" value="message:login"/>

</td>

</tr>

</table>

</form>

</body>

</html>

这个页面模板大多数是通常的HTML。我们从页面模板中可以看到Tapestry的区域化特性:它使用一个span元素,这个span元素带有一个叫key的属性,key的值映射到Login.properties文件里一个属性。一个Body控件被声明使用,因为它对客户端的JavaScript校验是必需的。

为Form component⑤设定delegate属性激活表单输入验证。delegate属性是我们在页面规范里声明的org.apache.tapestry.valid.IvalidationDelegate的实现类。如果验证错误发生了,我们用Conditional component③控件判断delegate是否有任何错误,如果有就把第一个错误④显示给用户。如果ognl 表达式ognl:beans.delegate.hasErrors 为true,Conditional控件将显示它的内容实体。所有的页面类和控件类都从AbstractComponent继承来一个叫beans的属性。这个beans属性是一个org.apache.tapestry.IbeanProvider的实例,利用它可以通过名字取得在页面规范文件里定义的beans.FieldLabel⑥被用于为inputuserName validField控件显示标签,这个FieldLabel控件也被用来与表单的验证代理协作,指出包含错误的输入域。

InputUserName⑦控件是一个显示控件的例子。显式控件是指在页面规范文件声明的控件。InputUsername和inputPassword控件都是显式的,它们与FieldLabel联合显示它们的displayName属性。

下面的是Login页的资源文件。Login.properties跟页面规范一并存放在WEB-INF目录。

Login.properties

title = Login to the Application

hint = Hint: Your password is your username spelled backwards.

login = Login

username = Username:

password = Password:

invalidpassword = Invalid Password

Here is the page specification for the Login page.

Login.page

<?xml version="1.0"?>

<!DOCTYPE page-specification PUBLIC

"-//Apache Software Foundation//Tapestry Specification 3.0//EN"

"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd>

<page-specification class="com.ociweb.tapestry.Login">

<bean name="delegate" class="org.apache.tapestry.valid.ValidationDelegate"/>①

<bean name="requiredValidator"②

class="org.apache.tapestry.valid.StringValidator">

<set-property name="required" expression="true"/>

<set-property name="clientScriptingEnabled" expression="true"/>

</bean>

<property-specification name="username" type="java.lang.String"/>

<property-specification name="password" type="java.lang.String"/>

<component id="inputUsername" type="ValidField"> ③

<message-binding name="displayName" key="username"/> ④

<binding name="validator" expression="beans.requiredValidator"/> ⑤

<binding name="value" expression="username"/> ⑥

</component>

<component id="inputPassword" type="ValidField"> ⑦

<message-binding name="displayName" key="password"/>

<binding name="validator" expression="beans.requiredValidator"/>

<binding name="value" expression="password"/>

</component>

</page-specification>

Page-specification元素的class属性和两个property-specification元素与Pig Latin翻译器应用是相似的。

你会发现第一个新东西-bean元素①,bean元素把一个org.apache.tapestry.valid.ValidationDelegate

的实例指定了名称"delegate"。页面HTML模板里的Form控件把它的参数delegate设定为

ognl:beans.delegate,就是指向了这个org.apache.tapestry.valid.ValidationDelegate实例。

Bean元素②把一个org.apache.tapestry.valid.StringValidator的实例指定了名称"

requiredValidator",以用于验证。这个bean的required属性被设为true表明使用这个bean的域是必须被验证的。这个bean的clientScriptingEnabled属性被设定为ture,表明使用这个bean的域客户端的JavaScript验证功能是激活的。RequiredValidator bean被用于验证inputUsername和inputPassword的内容。

控件inputUsername③被控件规范声明为ValidField,ValidField是一种用于Tapestry验证子系统的特殊版本的TextField控件。Message-binding元素被用于指定inputUsername控件的displayName参数的值,这个值是用"username"为关键字从login.properties④文件里得到。InputUsername控件的validator参数被设定为requiredValidator bean,这是我们在页面规范里声明过的⑤。控件的value参数跟页面Java类的username属性绑定在一起⑥。控件inputPassword的控件规范跟控件inputUsername几乎相似,除了用于取得displayName的关键字和绑定的页面Java类的属性不同。

通过使用ValidField控件和为表单(form)提供一个ValidationDelegate, 我们激活了Login表单的验证功能。除了服务器端的验证,Tapestry也提供了客户端的验证(利用JavaScript)。下面就是当用户提交一个表单而没有为UserName域提供值时,一个JavaScript错误对话框弹出时的屏幕抓图。

下面就是当用户提交一个表单而没有为Password域提供值时,一个JavaScript错误对话框弹出时的屏幕抓图。

下面就是Login页对应的Java 类。

Login.java

package com.ociweb.tapestry;

import org.apache.tapestry.html.BasePage;

import org.apache.tapestry.IRequestCycle;

import org.apache.tapestry.valid.ValidationConstraint;

import org.apache.tapestry.valid.IValidationDelegate;

public abstract class Login extends BasePage {

public abstract String getUsername();

public abstract void setUsername(String username);

public abstract String getPassword();

public abstract void setPassword(String password);

public void login(IRequestCycle cycle) {

String username = getUsername();

String password = getPassword();

StringBuffer sb = new StringBuffer(username);

String validPassword = sb.reverse().toString();

if (password.equals(validPassword)) {

cycle.activate("Success");①

} else {

String errorMessage = getMessage("invalidpassword");②

IValidationDelegate validationDelegate =

(IValidationDelegate) getBeans().getBean("delegate");③

validationDelegate.record(errorMessage,

ValidationConstraint.CONSISTENCY);④

}

}

}

跟Pig Latin翻译器应用一样,我们的页面类也是抽象的,它有抽象方法访问在页面规范里定义的属性(properties)。Tapestry会在运行时刻创建username和password属性。Login方法只是简单的验证一下用户输入的密码值是否刚好是用户名的反向。如果密码通过验证,用户将被引领导Success page①。

如果密码输入有误,我们用关键字"invalidPassword"通过从org.apache.tapestry.AbstractComponent里继承来的getMessage()方法从Login.properties②里查找对应的资源。我们需要把密码错误信息纪录到我们在页面规范中定义的页面validation delegate中去。我们可以利用我们在页面规范中指定的名称,从页面的beans属性中找回validationDelegate③。最后,我们调用org.apache.tapestry.valid.IvalidationDelegate的record方法把将要显示给用户看的错误信息保存起来。下面就是当用户输入错误密码的提交后的屏幕抓图。

下面就是Success页的页面模板。Success页的页面模板仅仅包含HTML标识,所以它不需要页面规范和页面

Java类。

Success.html

<html>

<head>

<title>Successful Login</title>

</head>

<body>

<p>

Congratulations! You have successfully logged on.

</p>

</body>

</html>

总结

我希望这篇文章已经向你展示了Tapestry框架在构建Web应用的是多么简单,然而优雅。Tapestry与大多数主流Web应用框架最大不同在于它让你用基于控件的方式开发,而非以操作为中心的方式开发。如果这篇文章激起了你的兴趣,我建议你把它下载下来利用它你自己的简单的Web应用。通过感受简单的应用,这是你了解这个框架的优点的唯一途径。如果你想在你的下一个项目里使用Tapestry,我强烈建议你购买

Tapestry In Action 这本书。我拥有这本书,对它我感到很满意。

References

1 Zip file with all source code and war files from the article. (12K)

http://www.ociweb.com/jnb/jnb2004_05.zip

2 Tapestry Home Page http://jakarta.apache.org/tapestry/

3 Tapestry In Action Page http://www.manning.com/lewisship/

4 Tapestry Wiki http://jakarta.apache.org/tapestry/wiki_frame.html

5 OGNL page http://www.ognl.org/

6 Tapestry Component Reference

http://jakarta.apache.org/tapestry/doc/ComponentReference/index.html

7 Tapestry Component Workbench http://www.t-deli.com/app

8 Designing Tapestry Mega-Components

http://www.onjava.com/pub/a/onjava/2001/11/21/tapestry.html

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有