项目的格局遵循的是Maven倡导的一个很合适的标准:
l Java源代码文件放在 src/main/java 下面
l Web应用程序文件放在 src/main/webapp(包括src/main/webapp/WEB-INF)
l Java测试资源放在src/test/java下面
l 非代码资源(包括Tapestry页面和组件模板)放在src/main/resources和src/test/resources下面
让我们来看看Maven根据原型创建了写什么,先从web.xml配置文件开始:
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!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>tutorial1 Tapestry 5 Application</display-name>
<context-param>
<!-- The only significant configuration for Tapestry 5, this informs Tapestry
of where to look for pages, components and mixins. -->
<param-name>tapestry.app-package</param-name>
<param-value>com.example.tutorial1</param-value>
</context-param>
<!--
Specify some additional Modules for two different execution
modes: development and qa.
Remember that the default execution mode is production
-->
<context-param>
<param-name>tapestry.development-modules</param-name>
<param-value>
com.example.tutorial1.services.DevelopmentModule
</param-value>
</context-param>
<context-param>
<param-name>tapestry.qa-modules</param-name>
<param-value>
com.example.tutorial1.services.QaModule
</param-value>
</context-param>
<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry5.TapestryFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>app</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
这个文件比较简短:你可以看到自己早先提供的包名作为 tapestry.app-package 上下文参数显示在这个文件中;TapestryFilter实例会利用这个信息订购page和component的Java类。
Tapestry操作的是一个servlet Filter而不是传统的servlet。用这种方法,Tapestry就有机会拦截到所有的传入请求,以据此决定哪个请求对应到哪个Tapestry页面(或者其它的资源)。最终的效果就是你不必为Tapestry维护任何额外的配置了,无论你要向应用程序添加多少page和component。
web.xml剩余的大部分都是配置用来匹配Tapestry执行模式对应的模块类的。执行模式定义了应用程序将如何运行:默认的执行模式是“production”,不过web.xml还定义了两个模式:“development”和“qa”(代表“Quality Assurance”)。模块类将会针对这些执行模式而被加载,并能以各种方式修改应用程序的配置。本教程稍后会回过头来再来讲这个执行模式和模块类。
Tapestry的page至少包含一个普通的Java类和一个组件模板文件。
在web应用程序的根目录,有一个叫做“Index”的page江北被用于任何没有在上下文名称后面指定额外路径的请求。
Index Java 类
Tapestry对于哪里放置page类有非常特殊的规定。Tapestry将一个子包,“pages”添加到了应用程序根包(“com.example.tutorial1”)下面;用于page的Java 类就放在这儿。一次Java类的全称就是com.example.tutorial1.pages.Index。
src/main/java/com/example/tutorial/pages/Index.java
package com.example.tutorial1.pages;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Log;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.HttpError;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import org.slf4j.Logger;
import java.util.Date;
/**
* Start page of application tutorial1.
*/
public class Index
{
@Inject
private Logger logger;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@Property
@Inject
@Symbol(SymbolConstants.TAPESTRY_VERSION)
private String tapestryVersion;
@InjectPage
private About about;
@Inject
private Block block;
// Handle call with an unwanted context
Object onActivate(EventContext eventContext)
{
return eventContext.getCount() > 0 ? new HttpError(404, "Resource not found") : null;
}
Object onActionFromLearnMore()
{
about.setLearn("LearnMore");
return about;
}
@Log
void onComplete()
{
logger.info("Complete call on Index page");
}
@Log
void onAjax()
{
logger.info("Ajax call on Index page");
ajaxResponseRenderer.addRender("middlezone", block);
}
public Date getCurrentTime()
{
return new Date();
}
}
在这份代码里面有许多东西,Index page想要精力向你呈现Tapestry中一堆不同的理念。即使如此,这个类看起来还是相当的简单:Tapestry的page和component没有积累需要扩展,也没有接口需要实现,而仅仅只是一个纯粹的POJO(Plain Old Java Object)……属性和方法都带有一些特殊的命名约定和注解。
这其中你必须得满足Tapestry框架的要求:
l 需要把Java类放在预定的包中,这里就是com.example.tutorial1.page
l 类必须是public的
l 需要确保有一个public的,没有参数的构造器(这里Java编译器已经悄悄地为我们提供了一个)
l 所有的非静态属性都必须是private的
在运行这个应用程序时,如我们所见,page会展示当前的日期和时间,还有一些额外的链接。currentTime属性就是这些值的来源;很快我们会明白这个值是如何被模板引用到的,那样它就可以从page和输出那里获取到了。
Tapestry总是会把page对应到一个模板;它们缺少了对方都是没有用的。事实上,一个page中的component也是以同样的方式被对待的(此外component并不总是有对应的模板)。
你回常常听到模型-视图-控制器模式(MVC)。模板就是MVC中视图。而作为模型的page会暴露出可以在模板中被引用到的JavaBean。
让我们来看看component模板是如何在Java类上构建出完整的用户界面的。
Component模板
Tapestry的page是一个POJO Java 类同一个Tapestry component模板的组合。模板会有跟Java类相同的名称,不过后缀名会是.tml。因为这里的Java类是com.example.tutorial.pages.Index,所以模板文件就会被放置在src/main/resource/com/example/tutorail/pages/Index.tml。最终,Java类和component 模板文件都会被存储在用于部署的WAR文件的同一个目录之中。
Tapestry的component模板是形式良好的XML文档。这就意味着你可以利用上任何可用的XML编辑器。模板可能甚至会有一个DOCTYPE或者一个XML schema来验证模板page的结构是否正确。
注意Tapestry回用一个非验证性质的解析器来解析component模板:它只会检查形式是否良好:正确的语法,对应平衡的元素,属性值是在双引号中,注入此类。你自己决定要不要构建流程来执行某些类型的模板验证,而只要能顺利的解析,Tapestry还是会照常接受模板。
Tapestry component模板的大部分都像是一个普通的XHTML:
src/main/resources/com/example/tutorial1/pages/Index.tml
<html t:type="layout" title="tutorial1 Index"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd"
xmlns:p="tapestry:parameter">
<div class="hero-unit">
<p>
<img src="${asset:context:images/tapestry.png}"
alt="${message:greeting}" title="${message:greeting}"/>
</p>
<h3>${message:greeting}</h3>
<p>The current time is: <strong>${currentTime}</strong></p>
<p>
This is a template for a simple marketing or informational website. It includes a large callout called
the hero unit and three supporting pieces of content. Use it as a starting point to create something
more unique.
</p>
<p><t:actionlink t:id="learnmore" class="btn btn-primary btn-large">Learn more »</t:actionlink></p>
</div>
<div class="row">
<div class="span4">
<h2>Normal link</h2>
<p>Clink the bottom link and the page refresh with event <code>complete</code></p>
<p><t:eventlink event="complete" class="btn btn-default">Complete»</t:eventlink></p>
</div>
<t:zone t:id="middlezone" class="span4">
</t:zone>
<div class="span4">
<h2>Ajax link</h2>
<p>Click the bottom link to update just the middle column with Ajax call with event <code>ajax</code></p>
<p><t:eventlink event="ajax" zone="middlezone" class="btn btn-default">Ajax»</t:eventlink></p>
</div>
</div>
<t:block t:id="block">
<h2>Ajax updated</h2>
<p>I'v been updated through AJAX call</p>
<p>The current time is: <strong>${currentTime}</strong></p>
</t:block>
</html>
你一定得用跟component 类的名称Index的每个字母都一样的名称来命名你的component模板文件,也就是Index.tml。如果有一个字符错了,就有可能在某些操作系统(比如Mac OS X,Windows)上仍然有效,不过在其它的(Linux和大多数其它的)上面就不行了。这可真令人烦心,因为在Windows上面做开发,而部署到Linux和Solaris上面是很常见的事情,所以这一处还是小心为是。
在Tapestry中,对于诸如Index.tml这样的component模板,其目标就是尽可能想一个普通的,静态的HTML文件。(这里的静态static的意思是对比一个动态生成的Tapestry page,不用修改的意思)。
事实上,在许多情况下我们所期望的是,模板开始是一个静态的HTML文件,有web开发者创建出来,然后被组装作为一个动态的Tapestry page。
Tapestry在XML命名空间里面隐藏了非标准的元素和属性。按照约定,前缀“t:”被用于主命名空间,不过这不是必须的,任何你想要使用的前缀都可以。
这个简短的模板展示了Tapestry相当多的特性。
Quickstart原型的部分意图是展示一堆不同的功能特定、方法以及在Tapestry被用到的通用模式。因此确实我们是在一次性地用全部的东西来打动你。
首先,有两个XML命名空间是通常都要被定义的:
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd"
xmlns:p="tapestry:parameter"
第一个命名空间,“t:”,被用来识别Tapestry特定的元素和属性。尽管有了一个XSD(就是一个XML schema定义),不过不完整(原因很快就会解释)。
第二个命名空间,“P:”,是一种把相当多的模板标记为一个参数传入到另外的组件的方式。很快我们会展开来描述这个东西。
Tapestry component模板包含大多数标准的XHTML,它们不做修改就会被向下传递给玩野浏览器。模板的动态部分由component和expansion来呈现。
模板中的扩展(expansion)
让我们从exansion开始。Expansion是在渲染页面时包含一些动态输出的简便方式。Expansion默认会引用page的JavaBean中的属性。
<p>The current time is: ${currentTime}</p>
大括弧中的值是一个属性表达式。Tapestry使用其自己的属性表达式语言,富有表现力,快速,且类型安全。
Tapestry并没有使用反射来实现属性表达式。
更高级的属性表达式可以横向引用多个属性(例如:user.address.city),或者甚至调用公共方法。这里的expansion简单的读取了page的currentTime属性。
Tapestry遵行有Sun JavaBean规范定义的规则:属性的名称currentTime映射到两个方法:getCurrentTime()和setCurrentTime()。如果你省略了其中一个方法或者两个都省略掉,属性就会是只可读的(如这个示例中所示),或则是只可写的。(请牢记,不用管什么JavaBean的属性,它的方法才是关键;实例标量的名称,以至于它们是否存在,都无关紧要。)
Tapestry则更近一步:在匹配expansion中的属性到page的属性时,它会忽略大小写。在模板中我们可以用${currenttime}或者${CurrentTime}或者其它变体,而Tapestry仍旧是调用getCurrentTime()方法。
注意在Tapestry中没必要设置有什么对象拥有currentTime属性;一个模板或者一个page总是会组合在一起互相利用;表达式总是以page实例为根,在这种情况下,就是Index类的一个实例。
Index.tml模板包含的第二个expansion:
<p>${message:greeting}</p>
这里greeting并非是page的一个属性;它实际上是一个本地的消息键。每个Tapestry page和component都可以拥有其自己的消息目录(message catalog)。(还有一个全局的消息目录,稍后我们会描述一下。)
src/main/resources/com/example/tutorial/pages/Index.properties
greeting=Welcome to Tapestry 5! We hope that this project template will get you going in style.
消息目录对于在代码或者模板之外存储重复的字符串时非常有用,尽管主要目的同应用程序的本地化有关(这会在稍后的章节中有详细描述)。可能被多个page用到的消息可以被存储在应用程序的全局消息目录中,也就是src/main/webapp/WEB-INF/app.properties。
这个“message:”前缀并不是某种特殊情况;实际上有一些这样的绑定前缀被构建到了Tapestry,每一个都有特殊的目的。事实上,表达式中忽略半丁前缀就跟使用了“prop:”是一样的,意思是将绑定看作是一个属性表达式。
Expansion在用来获取一块信息并将其作为字符串渲染到客户端时是很有用的,不过Tapestry中重要的担子都放在了component里面。
模板中的组件(component)
Component以两种方式在component模板中表示:
l 作为一普通的元素,不过带有一个t:type属性,用来定义component的类型。
l 作为Tapestry命名空间中的一个元素,这种情况下元素的名称决定其类型。
这里我们使用了一个<html>元素来表示应用程序的Layout(布局)component。
<html t:type="layout" ...>
...
</html>
而对于 EventLink component,我们使用了Tapestry 命名空间中的一个元素:
<t:eventlink page="Index">refresh page</t:eventlink>
选哪中形式就是一种选择而已。在多数情况下,两者几乎是一样的。
跟其他地方一样,大小写是无关的。这里的类型(“layout”和“eventlink”)都是用的小写;实际的类名称是 Layout 和 EventLink。Tapestry会进一步将核心库的component同这个应用程序定义的component“混淆”;如此类型“layout”会被映射到应用程序的component类com.example.tutorial.components.Layout,而“eventlink”会被映射到Tapestry内置的org.apache.tapestry5.corelib.components.EventLink类。
Tapestry的component是使用参数来配置的;对于每个component,都有一堆参数,每一个都带有一个特殊的类型和目的。某些参数是必需的,其它是可选的。元素的属性被用来将参数绑定到特定的字面值,或者是page的属性。Tapestry在此处是很灵活的;你总是能够将属性放到Tapestry的命名空间中(使用“t:”前缀),不过在大多数情况下,没必要这么做。
<html t:type="layout" title="tutorial1 Index"
p:sidebarTitle="Framework Version" ...
这个将Layout component的两个参数,title和sidebarTitle对应绑定到了字面值“tutorial Index”和“Framework Version”。
Layout component将实际给浏览器发送最终的HTML;我们将会在稍后的章节中查看这个模板。这里要点是,page的模板被集成到了Layout component的模板中。下图展示了参数是如何被传到Layout component并被渲染成最终的页面的:
这里有点意思(也是Tapestry中的一个高级概念,稍后回头来再讲)的是我们可以见Index.tml的一块作为sidebar参数传入Layout component。这就是tapestry:parameter命名空间(也就是“p:”前缀)的用处;元素的名称被匹配到component的一个参数,而template的一整快被传入了Layout component……它决定了在其模板的哪个位置渲染这一块。
<t:eventlink event="complete" class="btn btn-default">Complete»</t:eventlink>
这一次是PageLink component的page参数被绑定到了字面值“Index”(就是这个page的名称)。其或被渲染成一个重新渲染这个page的URL,解释了当前时间是如何被更新的。你也可以创建到应用程序中其它page的链接,稍后的章节中我们会看到,且除了page名称意外,还可以将额外的信息附加到URL上。
一个魔术小把戏
现在是时候玩一个魔术小把戏了。修改Index.java,将getCurrentTime()方法修改成:
Index.java (部分)
public String getCurrentTime()
{
return "A great day to learn Tapestry";
}
确保你对修改进行了保存,然后在网页浏览器中点击刷新:
这是Tapestry早期的一个令人叫绝的特性,对component类的修改可以立即生效(一个我们称作动态类重新加载Live Class Reloading的特性)。无需重启。也无需重新部署。做出修改然后马上就能看到效果。没有什么能拖你的后腿,或者当你的道儿。
如果Live Class Reloading不起作用,看看Class Reloading中的Troubleshooting一节。
不过……要是你代码写错了呢?如果你把模板中的名称搞错了会怎样。可以试一试;就在模板中,将${currentTime}改成比方说${currenTime},看看你会得到什么结果:
这是Tapestry的异常报告页面。它相当的详细。清楚的指明Tapestry正在做什么,还将问题同模板中的特定行关联起来,在上下文中显示出来。Tapestry总是展开显示整个异常跟踪栈,因为异常的抛出、捕获和在其他异常中重新抛出是如此普遍。事实上,如果我们将页面向下只是滚动一点点,就可以看到有关这个异常的更多信息,还有一点点帮助信息:
这就是Tapestry行事方式的一部分:不仅指出正在做的是什么还有出了什么问题,甚至于还要帮助你找到解决方案;在这里它告诉你应该已经使用过的属性名称。
其详细程度表明应用程序已经被配置成development模式而不是production模式。在production模式中,异常报告只会简单的显示顶层的异常消息。不过,大多数应用程序更进一步,会对Tapestry如何处理和报告异常进行自定义。
Tapestry会显示最深层异常的跟踪栈,与此同时还有许多关于运行时华景的详细信息:关于当前请求,HttpSession(如果存在一个的话)的详细信息,甚至还会有所有JVM系统属性的详细清单。往下滚动就可以看到所有的这些信息。
接下来是:实现Hi-Lo猜谜游戏