Tapestry Snippets
1. Spilt Tapestry Application into Modules
在实际的开发过程中,随着规模的不断扩大,把所有的Page都放在一个目录下,不管是WEBROOT下或是/WEB-INF/下或是/WEB-INF/${servlet.name}/下,显然都不是很好的选择。 幸好在Tapestry中,我们可以把页面分门别类的放在各自的目录中。举个例子:
webroot │ Home.html │ Home.page └───auth Login.html Login.page
如上,我们把Login页面放置在auth目录中,那么此时Login页面的全名为auth/Login而不是简单的Login了 ,如我们在PageLink中引用:
<a jwcid="@PageLink" page="auth/Login" >Login</a>
2. Tapestry Specifications
所有的Specification,不管扩展名是什么,内容都是XML文件。
2.1. Application Specification - ${servlet.name}.application
每个应用程序一般只有一个。
2.2. Library Specification - ${library.name}.library
和Application Specification的结构基本相当,一般每个组件库的jar中一个。
2.3. Page Specification - ${page.name}.page
一个页面一个,应用程序是由许多Page组成。
2.4. Component Specification - ${component.name}.jwc
每个组件一个,如文本框,日期选择等。每个Page可以包含许多Component,每个Component中还可以包含许多Component。
3. 让Tapestry找到你的Application Specification
Application Specification,顾名思义,应用程序定义书。一般情况下,一个WEB应用程序只有一个。文件名为${servlet.name}.application。 其中${servlet.name}为web.xml中配置ApplicationServlet的Servlet名称,其放置的路径必须是/WEB-INF/${servlet.name}.application 或者/WEB-INF/${servlet.name}/${servlet.name}.application如:
<!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>Components for Tapestry Examples Web Application</display-name> ... <servlet> <servlet-name>comp4t-examples</servlet-name> <servlet-class>org.apache.tapestry.ApplicationServlet</servlet-class>. <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>comp4t-examples</servlet-name> <url-pattern>/app</url-pattern> </servlet-mapping> ... </web-app>
这样配置的web.xml文件的话,那么Application Specification应该为comp4t-examples.application, 其位于的路径为/WEB-INF/comp4t-examples.application或者/WEB-INF/comp4t-examples/comp4t-examples.application
4. 让Tapestry找到你的Page Specification
4.1. 置Page Specification于Application Specification相同路径中
最简单,最直接的办法就是这个,只要和Application Specification保持相同路径,就不怕Tapestry找不到它。
4.2. 置Page Specification于/WEB-INF/中
4.3. 置Page Specification于/WEB-INF/${servlet.name}/中
4.4. 置Page Specification于/中,即WEB应用程序最根目录中
4.5. 置Page Specification于相应java文件路径下,但需Application/Library Specification中配置其相应路径。
一般可以放置相应的java文件相同的路径下,如com.javaforge.comp4t.examples.FrameworkComponentExamples, 那么把Page Specification放在/com/javaforge/comp4t/examples/下比较整齐。然后须在Application Specification指定。
<application name="Components for Tapestry Examples"> ... <page name="FrameworkComponentExamples" specification-path="/com/javaforge/comp4t/examples/FrameworkComponentExamples.page"/> <page name="Home" specification-path="/com/javaforge/comp4t/examples/Home.page"/> ... </application>
这样的好处是所有的Tapestry Page都列在了Application Specification。便于管理和整理。
5. 管理服务器端状态
每次提交中需要保存以备后续请求处理中使用的数据。
5.1. 保持住Page的属性(Page属性的持久化)
提到Session,大家不会陌生,因为我们要想在其他的服务请求中得到之前的状态,就用到它,最典型的就是用户验证后的登陆信息。 最普通的做法就是在用户正常的Login之后,把用户的信息放到Session中,在后面处理中需要用户信息时,从Session中取出。 在Tapestry中是不推荐直接使用Session的。框架的作用是什么?就是用封装,抽象还使你需要做的工作得到简化。
下面是一个简单的例子,在Tapestry中我们不再需要和Session打交道。Tapestry已经为你做好了这一切。
public abstract class FrameworkComponentExamples extends BasePage { ... public abstract List getRecords(); public abstract void setRecords(List records); ... }
<page-specification class="com.javaforge.comp4t.examples.FrameworkComponentExamples"> ... <property name="records" persist="session"/> ... </page-specification>
这样的配置非常明确的告诉了Tapestry将FrameworkComponentExamples页面的records属性放置到Session中。在以后的调用中, 只要你调用了getRecords()方法,那么Tapestry会从Session中取出这个对象, 要是你调用了setRecords(myList), 那么Tapestry会将myList设置到Session中,那么Session中的键值是如何维护的呢,我们并没有提供给Tapestry呀,一句话,Tapestry自己拼凑出来的。 后面提到拼凑方法。
需要注意的一点,我们的Page中的属性必须是abstract的, Tapestry文档中会说到,不用abstract也可以,但我告诉你,最好使用上面的写法。 想象一下,只有是抽象的,不提供实现,才有可能是从Session或Client中取得属性值, 如果你给他写了实现, 而你的实现很可能定义一个成员变量,那到底是从你这个成员变量中取,还是从Session中取。
5.2. 关于Page属性的Session Key
拼凑的方法是${applicationId},${pageName},${idPath},${propertyName}其中${idPath}可能没有,
${applicationId}为前面提到的Servlet名,即web.xml中配置的ApplicationServlet的Servlet名,如comp4t-examples。
${pageName}为页面名,如FrameworkComponentExamples。
如果该页面中调用和其他组件,而且组件中的某个属性指定了持久化(persist)属性,那么${idPath}就会存在。
${propertyName}为属性名,如records。
这样的话整个Session Key为comp4t-examples,FrameworkComponentExamples,records,不会重复。
5.3. 页面属性的持久化策略(PersistenceStrategy)
tapestry.persist.xml
<contribution configuration-id="PersistenceStrategy"> <strategy name="session" object="service:SessionPropertyPersistenceStrategy"/> <strategy name="client" object="service:PageClientPropertyPersistenceStrategy"/> <strategy name="client:page" object="service:PageClientPropertyPersistenceStrategy"/> <strategy name="client:app" object="service:AppClientPropertyPersistenceStrategy"/> </contribution>
6. 构造友好的URL。
何为不友好的URL,如
/comp4t-examples/app?component=%24DirectLink&page=Home&service=direct&sp=S123456
/comp4t-examples/app?digest=a4008189d74c98218e216c1e2ce53193&path=%2Fcom%2Fjavaforge%2Fcomp4t%2Fexamples%2Fstyle.css&service=asset
上面的的两例称之为非友好URL,为什么呢,太乱了,不仔细分析是无法弄清其中道理,那为什么会存在呢?因为机器能看懂。虽然乱,但也是标准的URL。
看看下面两例
/comp4t-examples/Home,$DirectLink.d?sp=S123456
/comp4t-examples/assets/a4008189d74c98218e216c1e2ce53193/com/javaforge/comp4t/examples/style.css
虽然没有达到最佳效果,但是进步是很明显的,他和上面的可以表达相同的信息量,因为都能被Tapestry解释。
当我们不对Tapestry进行任何配置的时候,在程序运行过程中,出现的URL是第一种,也是在Tapestry 3.x中固定使用的一种。 但在Tapestry 4.0中有了很大的改观,只要稍加配置,即可形成第二种的效果。
或许你对Hivemind还不了解, 但是我不得不告诉你,Tapestry 从4.0 开始基于Hivemind, 可以这样说,不了解Hivemind, 你便精通不了Tapestry。 虽然你很可能不想涉入新的东西,但是新的东西也会给你带来新的效果。 你的学习不会浪费。
下面就是一段Hivemind配置,来达到第二种URL效果。位于classpath:/META-INF/hivemodule.xml文件中
<module id="comp4t.examples" version="1.0.0" package="com.javaforge.comp4t.examples"> ... <contribution configuration-id="tapestry.url.ServiceEncoders"> <page-service-encoder id="page" extension="html" service="page"/> <direct-service-encoder id="direct" stateless-extension="d" stateful-extension="sd"/> <asset-encoder id="asset" path="/assets/"/> </contribution> ... </module>
其实很多时候,我们并不需要知道是什么意思,只需要知道代码要写的位置,和能起到的效果。 但是如果你真想知道其中原委,那我给你解释。 TODO:解释
7. 对象和字符串之间的转换 - DataSqueezer
关联源文件: tapestry.data.xml
在WEB应用中,不可避免出现对字符串和对象进行转换,因为浏览器表单数据或者是HTTP URL,或者Cookie全部都是字符串,而我们的应用中, 非常普遍的做法,就是对提交上来的数据和对象进行绑定,在Struts和Spring中用表单中项目的名称和对象的属性进行对应(并做了简单原始类型的转换)。 但在Tapestry中做法是不同的,DataSqueezer和SqueezeAdaptor起着关键性的作用。
我们简单看一下Tapestry中的Test,就不难发现其中转换的规律。
org.apache.tapestry.junit.utils.TestDataSqueezer
public void testBoolean() { attempt(Boolean.TRUE, "T"); attempt(Boolean.FALSE, "F"); } public void testNull() { attempt(null, "X"); } public void testByte() { attempt(new Byte((byte) 0), "b0"); attempt(new Byte((byte) -5), "b-5"); attempt(new Byte((byte) 72), "b72"); } public void testFloat() { attempt(new Float(0), "f0.0"); attempt(new Float(3.1459), "f3.1459"); attempt(new Float(-37.23), "f-37.23"); } public void testDouble() { attempt(new Double(0), "d0.0"); attempt(new Double(3.1459), "d3.1459"); attempt(new Double(-37.23), "d-37.23"); } public void testInteger() { attempt(new Integer(0), "0"); attempt(new Integer(205), "205"); attempt(new Integer(-173), "-173"); } public void testLong() { attempt(new Long(0), "l0"); attempt(new Long(800400300l), "l800400300"); attempt(new Long(-987654321l), "l-987654321"); } public void testShort() { attempt(new Short((short) 0), "s0"); attempt(new Short((short) -10), "s-10"); attempt(new Short((short) 57), "s57"); } /** @since 2.2 * */ public void testCharacter() { attempt(new Character('a'), "ca"); attempt(new Character('Z'), "cZ"); } public void testString() { attempt("Now is the time for all good men ...", "SNow is the time for all good men ..."); attempt("X marks the spot!", "SX marks the spot!"); attempt("So long, sucker!", "SSo long, sucker!"); } public void testComponentAddress() { ComponentAddress objAddress = new ComponentAddress("framework:DirectLink", "component.subcomponent"); attempt(objAddress, "Aframework:DirectLink,component.subcomponent"); objAddress = new ComponentAddress("framework:DirectLink", null); attempt(objAddress, "Aframework:DirectLink,"); }
每种类型都对应一个或者一串前缀,一个字符串如果开头单个字母是前缀中的一个的时候,那么就被确定为该类型,如"T"是"TF"中之一,那么"T"字符串被确定为Boolean形。
Table 1. SqueezeAdaptor
类型 | Adaptor | 前缀 | 举例 |
---|---|---|---|
null | X | X | |
Boolean | BooleanAdaptor | TF | T, F |
Byte | ByteAdaptor | b | b127 ,b0, b-5 |
Character | CharacterAdaptor | c | ca, cZ |
Double | DoubleAdaptor | d | d0.0, d3.1459, d-37.23 |
Float | FloatAdaptor | f | f0.0, f3.1459, f-37.23 |
Integer | IntegerAdaptor | -0123456789 | 0, 205, -173 |
Long | LongAdaptor | l | l0, l800400300, l-987654321 |
Short | ShortAdaptor | s | s0, s-10, s57 |
String | StringAdaptor | S | SNow is the time, SX marks |
Serializable | SerializableAdaptor | OZ | O123NWEQ..., Z23423EXF... |
ComponentAddress | ComponentAddressAdaptor | A | Aframework:DirectLink,component.subcomponent |
8. Form处理的核心阶段Rewind
所有的Form组件(Form Control Component),如:文本框,文本域,提交按钮,选择列表等等(但是Form本身不属于此列,它只是一个装Form组件的容器) 都继承自AbstractFormComponent, 在AbstractFormComponent中有一个非常重要的方法rewindFormComponent。 当画面中的Form进行提交时,第一个阶段就是Rewind阶段。 Form对象会首先进行循环调用其中所有的Form组件的rewindFormComponent方法。
根据Form组件的性质,每个Form组件中的rewindFormComponent方法的实现是不同的, 如文本框,列表,单选,多选等都是简单的把提交上去的值重新付给这些组件对象, 并且在同时进行了对Page中相应属性的绑定(即把这些值更新到Page的属性当中去)。但是如提交按钮,提交链接中所做的则是把Template中指定的监听加到Form对象当中去, 以便Rewind过程结束之前,调用这些Page中的监听方法(FormSupportImpl的runDeferredRunnables()方法)。
9. Tapestry中的Validation
9.1. Tapestry中提供的Validator
Tapestry中提供了许多Validator,我们直接使用他们就可能完成大部分通常的Check。
关联源文件tapestry.form.validator.xml<contribution configuration-id="Validators"> <validator name="email" configurable="false" class="Email"/> <validator name="required" configurable="false" class="Required"/> <validator name="max" class="Max"/> <validator name="maxDate" class="MaxDate"/> <validator name="maxLength" class="MaxLength"/> <validator name="min" class="Min"/> <validator name="minDate" class="MinDate"/> <validator name="minLength" class="MinLength"/> <validator name="pattern" class="Pattern"/> </contribution>
9.2. 简单的例子
<input type="text" jwcid="minAndMax@TextField" value="ognl:minAndMax" translator="translator:number,pattern=#####" displayName="literal:Min,Max" validators="validators:min=3,max=5"/>
上面是一个非常简单的例子,达到的目的就是改组件对应的Page的int属性minAndMax必须在3和5之间,由于Page的minAndMax属性为int型, 不是字符串,所以我们必须告诉Tapestry如何将页面中的值转换为Page的属性,所以这里面就指定了translator。
9.3. Validation Specification - validators属性的格式
里面的内容前缀首先是validators:,Validator之间用逗号分隔,每个Validator的格式可以是下面形式
- name
- name=value
- name[message]
- name[%message-key]
- name=value[message]
- name=value[%message-key]
- $name
其中name为Validator名,如:email, required, max, maxDate, min等等。
value为传入的参数值,如:min=3, max=5等。
message为当该Validator校验失败后显示的Message。
message-key为本地化文件中Message的Key值, 如Tapestry Page Name为Login的话, 那么相应的有Login.html,Login.page,Login.properties,Login_zh_CN.properties等,这里面的两个properties文件就是本地化文件。
下面是具体的例子
ValidationExamples.html中
<input type="text" jwcid="minAndMaxWithMessage@TextField" value="ognl:minAndMaxWithMessage" translator="translator:number,pattern=#####" displayName="literal:Min,Max" validators="validators:min=3[%must-larger-than-3],max=5[Must less than 5]"/></td>
那么在ValidationExamples.properties中为
must-larger-than-3=Field {0} must larger than 3.
需要注意的是,其中的{0}参数会被自动用Field名替换。
$name中的name是对一个bean的引用,该bean可以在Page Specification中定义。
如在ValidationExamples.page中定义为
<bean name="zipValidator" class="org.apache.tapestry.form.validator.Pattern"> <set name="pattern" value="message:zip-code-pattern"/> <set name="message" value="message:zip-code-message"/> </bean>
那么在ValidationExamples.html中就可以像下面这样引用。
<input type="text" jwcid="pattern@TextField" value="ognl:pattern" displayName="literal:Pattern" validators="validators:$zipValidator"/>
10. 特殊用语的解释
10.1. classpath:/META-INF/hivemodule.xml中的classpath
表示在classpath下/META-INF/中的hivemodule.xml文件 , 所谓classpath,在WEB应用程序中,通常指/WEB-INF/lib下的每一个jar中,或者/WEB-INF/classes下。
10.2. ${servlet.name}.application中的${}
是一个替代符号,或者叫变量名,上面的表达表示一个通用的名字,如在应用程序中可能具体为MyTapestryExamples.application, PetStore.application等。