通过前面章节的学习,我们了解到Velocity主要用作MVC框架View组件的脚本语言。Servlets是Java下MVC框架里用得最多的编程方式。通过把Servlets用作控制器(controller),Velocity用作显示层(View),把Servlets或JavaBeans作用模型(model),这样就创建了一个理想的Web开发全面解决方案。在这一章里,我们将学习如何在Servlets里使用Velocity。 使用Servlets 在开始学习这章内容之前,你必须充分理解Servlets是什么?以及它是如何工作的?如果对此你已经很熟悉了,那么你可以直接跳到“用VelocityServlet扩展Servlets”节进行深入学习。 在动态Web页面开发的早期,服务端语言,比如ASP、JSP等主要用于在HTML页面里嵌入服务器端执行的代码。当浏览器请求这个页面的时候,服务器立即对所请求的ASP/JSP页面进行解析,执行里面的语句,并向用户返回一个纯粹的HTML页面,有时还要附带返回客户端执行的JavaScript或VBScript脚本。混合HTML和服务器端代码并不容易,而且,服务器端脚本语言还要受到一些限制,比如服务器平台支持、服务器支持等比较有限。 Servlets把代码从HTML中提取出来,把它放到服务器上,这样,你就可以使用全功能的Java语言进行Servlets逻辑开发。Servlets也能进行分布式处理,而且可以使用模板来向用户提供信息显示。 Servlets的通用格式 在进行深入学习之前了解传统的Servlets结构是非常有必要的。Listing 12.1显示了一个简单的Servlets。 import java.io.*; import java.sql.*; import javax.servlet.*; import javax.servlet.http.*; import javax.naming.*; public class ViewAccount extends HttpServlet { public void init() throws ServletException { } public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<HTML>"); out.println("</HTML>"); } catch (SQLException e) { e.printStackTrace(); } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } Listing 12.1 传统servlet代码 当有一个web浏览器请求达到时,一般情况下,请求的方式为POST或GET。在上面的代码中你可以看到,其中有一些方法将用于处理这两种请求,分别为doPost()和doGet()方法,其中doPost()方法只是简单委派给了doGet()方法。在doGet()方法内部有两个重要的变量:request和response。request对象用于把用户信息转换成servlet能够理解的信息。特别的,用户在HTML窗体里的输入将作为文本进行处理。response对象主要负责向用户的浏览器返回标准的HTML页面。response对象返回的信息包括HTML、XML和图片。response对象也可以和PrintWriter对象一起协同工作,以用于写入信息并返回给用户。 Servlets不能使用普通的Web服务器作为主机,只能使用应用程序服务器(application servers)作为主机。可以作为Servlets主机的软件有Tomcat、JBOSS、Resin、WEBlogic、apusic等。在很多情况下,应用程序服务器被用于和Web服务器一起协同工作,比如IIS或Apache等。当Web服务器得到一个servlet请求时,这个请求被移交给应用程序服务器执行。应用程序服务器使用java编译器来创建一个可执行的Servlets映像,这个Servlets映像将在JVM里执行。我们将在下一章演示这个话题的示例。 用VelocityServlet扩展Servlets 在某种程度上,你可以使用Velocity引擎和servlet,通过Velocity模板语言书写代码来为用户创建信息输出。为了让Servlets的使用更容易,我们将通过使用一个名叫VelocityServlet的基类和名叫handleRequest()的方法来代替传统Servlets的HttpServlet和doGet()/doPost()方法。 handleRequest()方法传递三个参数HttpServletRequest、HttpServletResponse和Context来响应GET/POST请求。HttpServletRequest、HttpServletResponse对象和doGet()、doPost()方法接受的是同一个对象(代码见Listing 12.1)。上下文对象是一个用于在Servlets里使用的Velocity引擎的上下文。我们将在上下文对象里为用于向用户显示响应的Velocity模板放置信息。 handleRequest()方法返回一个模板对象,该方法自动把传递进来的上下文对象进行合并。因而,在handleRequest()方法里的代码,主要完成设置上下文和合并返回的Velocity对象等操作。如果返回的模板对象为null,这些代码就不能完成合并操作。在这种情况下,和传统的Servlets代码一样,这些代码将向用户浏览器返回一些放置在response对象里的PrintWriter对象里的东西。 基本的Velocity Servlet代码 handleRequest()方法示例代码见Listing 12.2,用于向浏览器简单输出字符串。 import java.util.Vector; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.velocity.Template; import org.apache.velocity.context.Context; import org.apache.velocity.servlet.VelocityServlet; import org.apache.velocity.exception.*; public class VelocityServletExample extends VelocityServlet { public Template handleRequest( HttpServletRequest request, HttpServletResponse response, Context context ) { Vector v = new Vector(); v.add("one"); v.add("two"); v.add("three"); context.put("list", v); Template template = null; try { template = getTemplate("displaylist.vm"); } catch( Exception e ) { PrintWriter out = response.getWriter(); out.println("Error getting template"); } return template; } } Listing 12.2 A Velocity servlet example. 当浏览器调用Listing 12.2里的代码时,初始化一个Velocity对象,并在里面放置了三个字符串对象。Velocity对象通过下面的语句被放入上下文中。 context.put("list", v); 注意,我们在上下文里放置的是Vector对象实体(java.util.Vector)。接着,声明了一个模板对象,并使用getTemplate(String)方法来从服务器的磁盘上定位和加载模板。如果在获取模板文件的过程中出现错误时,response对象里的PrintWriter被获得,并向其中写入错误信息。最后一个操作是返回模板对象。如果某些错误导致这个对象为null,系统将不能完成合并操作;否则,从服务器读入的模板将和上下文一起被合并。Listing 12.3为这个示例所使用的Velocity模板。 <?xml version="1.0" encoding="ISO-8859-1" ?> <list> #foreach( $value in $list ) <number>$value</number> #end </list> Listing 12.3 The displaylist.vm Velocity code. Listing 12.3里的模板不是HTML格式的,我们使用XML来演示Velocity模板并不仅限于使用HTML格式。了解这个示例是如何执行的非常重要。还记得“list”关键字和Vector对象关联,在模板里的Velocity模板语言代码通过#foreach循环指令使用Vector。回想一下在List对象(如Vector)里使用#foreach指令的定义我们就可以发现,迭代器(iterator)自动提取并用于获取所有在List(在这种情况下还可以是Vector)里的独立对象。 所有来自Vector的每一个对象都被提出并放置到引用$value处。既然字符串不是混合对象,我们就可以简单地输出它的值(在<number>元素标记里)。Figure 12.1演示了执行结果。
HttpServletRequest和HttpServletResponse对象被传递给Servlets代码,同时被放置在两个上下文常量对象中: ■ VelocityServlet.REQUEST—Stored as req ■ VelocityServlet.RESPONSE—Stored as res
每一个对象都能通过直接调用引用名(req和res)的方式用于Velocity模板中。 #set($username = $req.getParameter('username')) 创建一个MVC应用程序 我们的第一个Velocity示例相当简单,现在我们来搞一个稍复杂一点的示例。在这一节里,你将使用Servlets来构建一个MVC应用程序,其中:Servlets用作控制器(controller)、Velocity模板作用视图(view)、JavaBeans作用模型(model)。这个应用程序是一个具有以下四个功能的CD数据库: ■增加一个CD,同时返回一个thank-you响应 ■为特定的CD增加歌曲track,同时返回一个thank-you响应 ■通过特定的艺术家返回其所有的CD ■获取特定CD的全部歌曲track Figure 12.2显示了增加一个新CD时的情况;Figure 12.3显示了当你使用特定的艺术家搜索CD时的情况;Figure 12.4显示了一个特定CD的所有歌曲时的情况。
数据库结构 这个应用程序使用了一个数据库来保存CD和所有歌曲的信息。在这个测试环境,我们选择MYSQL数据库来存在数据,当然,你也可以选择其他类型的数据库。下面的SQL建表命令显示了CD和歌曲表的数据结构。 create table cd ( id int not null primary key auto_increment, title varchar(128), artist varchar(64), tracks int); create table tracks( id int not null primary key auto_increment, cd_id int, name varchar(64), length varchar(16));
数据库访问 我们的模型组件打算使用实体EJBs来访问CD表和歌曲(tracks)表的数据,这些bean和Servlets被用于向用户提供服务。在这个测试环境里,我们使用了Resin应用程序服务器。EJBs通过JNDI(Java名字和目录接口)资源引用来访问数据库。Listing 12.4显示了在应用程序服务器的配置文件中增加的 <resource-ref>元素。除了数据库驱动和URL外,这个元素使用的是标准规范。如果你使用的是另外数据库,你需要改变<init-param>元素以满足需要。 <resource-ref> <res-ref-name>jdbc/ProductsDB</res-ref-name> <res-type>javax.sql.ConnectionPoolDataSource</res-type> <init-param driver-name="org.gjt.mm.mysql.Driver"/> <init-param url="jdbc:mysql://localhost:3306/products"/> <init-param user=""/> <init-param password=""/> <init-param max-connections="20"/> <init-param max-idle-time="30"/> <init-param max-active-time="1"/> <init-param max-pool-time="1"/> <init-param connection-wait-time="1"/> </resource-ref> Listing 12.4 The resin.config resource text. 模型(Model)代码 在这里,你已经创建了一个数据库,同时包含了JNDI引用,现在,通过JNDI引用,你就可以访问数据库了。是时候考虑实体beans了,我们将把它用于访问数据库表里的数据。因为在我们的有两个数据库表,因此就需要构建两个EJB。为了达到目的,让我们利用Resin的container-managed persistence (CMP)模式来创建bean,同时主要把精力集中在实现EJB本地实例上。意思是我们只处理CMP的本地接口,而不处理远程接口,因此只需要很少的代码。由于本书是关于Velocity的,故我们只展示这两个bean的代码,不作更多解释,你可以在站点http://www./compbooks/gradecki处下载这个完整的示例代码。下面就让我们来看一看EJB文件的定义(Listing 12.5)。 <ejb-jar> <enterprise-beans> <entity> <ejb-name>CDRecordBean</ejb-name> <local-home>cd.CDRecordHome</local-home> <local>cd.CDRecord</local> <ejb-class>cd.CDRecordBean</ejb-class> <prim-key-class>int</prim-key-class> <primkey-field>id</primkey-field> <persistence-type>Container</persistence-type> <reentrant>True</reentrant> <abstract-schema-name>CDTable</abstract-schema-name> <sql-table>cd</sql-table> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>title</field-name></cmp-field> <cmp-field><field-name>artist</field-name></cmp-field> <cmp-field><field-name>tracks</field-name></cmp-field> <query> <query-method> <method-name>findByArtist</method-name> </query-method> <ejb-ql>SELECT o FROM CDTable o WHERE o.artist like 1</ejb-ql> </query> </entity> <entity> <ejb-name>TracksRecordBean</ejb-name> <local-home>cd.TracksRecordHome</local-home> <local>cd.TracksRecord</local> <ejb-class>cd.TracksRecordBean</ejb-class> <prim-key-class>int</prim-key-class> <primkey-field>id</primkey-field> <persistence-type>Container</persistence-type> <reentrant>True</reentrant> <abstract-schema-name>TrackTable</abstract-schema-name> <sql-table>tracks</sql-table> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>cd_id</field-name></cmp-field> <cmp-field><field-name>name</field-name></cmp-field> <cmp-field><field-name>length</field-name></cmp-field> <query> <query-method> <method-name>findByCdID</method-name> </query-method> <ejb-ql>SELECT o FROM TrackTable o WHERE o.cd_id=?1</ejb-ql> </query> </entity> </enterprise-beans> </ejb-jar> Listing 12.5 The CDRecordBean EJB file. Listing 12.5里的EJB文件展示了这两个bean是如何定义的,其中包含了主键和每一个表的字段。这两个实体bean都包含了一个<query>元素,它允许数据从表中取出并用于除主键以外的字段。为了让你了解在Resin应用程序服务器里是如何书写实体bean的,让我们来看一下Listing 12.6里的CDRecordBean类。 package cd; import javax.ejb.*; public abstract class CDRecordBean extends com.caucho.ejb.AbstractEntityBean { public abstract String getTitle(); public abstract String getArtist(); public abstract int getTracks(); public abstract int getId(); public abstract void setTitle(String title); public abstract void setArtist(String artist); public abstract void setTracks(int tracks); public abstract void setId(int id); public int ejbCreate(String title, String artist, int tracks) throws CreateException { setId(0); setTitle(title); setArtist(artist); setTracks(tracks); return 1; } public void ejbPostCreate(String title, String artist, int tracks) { // since there are no relations, this is empty. } } Listing 12.6 The CDRecordBean class. Listing 12.6里的CDRecordBean类继承自AbstractEntityBean类,AbstractEntityBean类是Resin制造商定义的一个助手类。这个助手类提供了所有的用于让实体bean实现空body的通用方法。事实上,把该助手类用于你的bean时,你只需要提供一个方法,从而减少代码量和代码混杂。另外,bean里剩余的代码被用于定义与这个bean相关联的表字段。Listing 12.7显示了TracksRecordBean类。 package cd; import javax.ejb.*; import java.sql.*; public abstract class TracksRecordBean extends com.caucho.ejb.AbstractEntityBean { public abstract int getId(); public abstract int getCd_id(); public abstract String getName(); public abstract String getLength(); public abstract void setId(int id); public abstract void setCd_id(int cd_id); public abstract void setName(String name); public abstract void setLength(String tracks); public int ejbCreate(int cd_id, String name, String length) throws CreateException { setCd_id(cd_id); setName(name); setLength(length); return 1; } public void ejbPostCreate(int cd_id, String name, String length) { // since there are no relations, this is empty. } } Listing 12.7 The TracksRecordBean class. 我们在之前曾经提及,你可以为这些实体bean下载剩余的文件。所有的实体文件被放置在Resin 应用程序服务器主机的/classes目录下。 The View Code 现在,你需要考虑一下如何使用Velocity作为你的脚本语言来进行输出。这里将是你施展魔法的地方,为用户提供令人激动的输出。对这个应用程序来说,我们需要三个Velocity模板: ■ thanks.vm—显示thank-you信息的普通页面 ■ displaycd.vm—向用户显示CD列表的页面 ■ displaytracks.vm—显示特定CD内所有歌曲的页面 首先,让我们来看一下thanks.vm模板,见Listing 12.8 <HTML> <HEAD> <TITLE></TITLE> <link rel="stylesheet" type="text/css" href="defaultpage.css"> </HEAD> <BODY BGCOLOR="#F79C19" link="ffffff" alink="999999" vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0"> <BR> #if ($message) $message #end $thanks <img src="/images/tune_big.gif" height="250" width=283></td></tr> </table> </BODY> </HTML> Listing 12.8 The thanks.vm template. 这个thanks.vm模板事实上是web框架集的一部分,它提供了所有可见的边框。这个模板将被放置在main处,或放在body里,是web框架集的一部分。在这里,为了让页面更美观,你需要提供适当的背景颜色和图片。在这个模板的顶部是页面背景的信息,后跟实际的Velocity元素。 第一个元素为带有$message引用的#if指令。$message引用用于显示一个信息(当一个错误出现并且你需要让用户知道这个错误的时候)。这个信息只有在错误发生时才会被写入上下文中,因此,程序没有发生错误的时候,这个引用在上下文里是不存在的。如果我们不用#if指令对$message进行测试,那么在输出中就有可能出现文本字符串“$message”,这看起来不太好,因此我们使用$if指令对$message引用是否包含有值进行测试,如果$message引用包含有值(意思是在上下文里可以找到这个引用),那么就显示$message引用的值。 在发生错误的情况下,你仍旧想感谢用户辛苦输入新的CD或歌曲。这个输出就$thanks引用来产生。
显示CD 我们的应用程序允许用户通过特定的艺术家搜索数据库里所有的CD,并得到一个这些CD的列表。在每一次显示CD列表的时候都将显示一个按钮,以方便用户可以单击来列出特定CD的歌曲列表。Listing 12.9里的displaycd.vm模板用于处理这些显示任务。 <HTML> <HEAD> <TITLE></TITLE> <link rel="stylesheet" type="text/css" href="defaultpage.css"> </HEAD> <BODY BGCOLOR="#F79C19" link="ffffff" alink="999999" vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0"> <BR><BR> <font color="ffffff"> #foreach($value in $cds) <form action="http://localhost:8080/cd/cdVelocityHandler" method="post"> <b> Title: $value.title </b> <input type="hidden" name="id" value="$value.id"> <input type="submit" name="submit" value="tracks"> </form> #end <BR> <img src="/images/tune_big.gif" height="250" width="150"> </BODY> </HTML> Listing 12.9 The displaycd.vm template. 和thanks.vm模板一样,在这个模板的开头部分包含了一些HTML标记。所有CD的输出将被显示在框架集的body内。伴随HTML而来的是Velocity代码。现在让我们回忆一下需要列表显示的两个部分:显示某艺术家的所有CD标题和显示一个用于列出每个CD的所有歌曲的按钮。 假如你正和你的web开发者一起工作,你决定让代码执行一个操作,把从数据库里提出来的CD放到一个名叫$cds的Collection对象里,并将这个$cds对象放入上下文中。这个Collection对象将包含许多基于某特定艺术家所有CD的CDRecordBean对象。 正如你已经看到的,#foreach指令用于从可用的对象里提取一个迭代器(iterator),Collection类就是这样的一个对象。因此,每一次循环,$value引用将得到一个CDRecordBean实体对象。利用bean的getter方法,适当的值将被提取出来。 许多工作都发生在循环内部,在这里将从数据库提取用于显示的CD标题。注意,一定要用HTML的<form>标记包围这个标题,这个<form>标记用于显示一个让用户单击就可以显示所有这张CD歌曲的按钮。 在讨论完应用程序的需求后,你和web开发者决定从CD表里提取ID,用于链接歌曲表。为了实现这个想法,你在<form>元素里创建了一个隐藏的<input>元素。注意,这个隐藏输入按钮的值用的是$value.id,意思是你可以向控制器传递每一个CD的ID。 显示歌曲(tracks) 当用户单击displaycd.vm模板输出的Tracks按钮时,将显示歌曲的名称和歌曲长度。Listing 12.10显示的displaytracks.vm模板将用于完成这些任务。这个模板再次包含了HTML标记,以便更美观地显示这个模板。在HTML标记之后,你将看到另一个#foreach循环。 <HTML> <HEAD> <TITLE></TITLE> <link rel="stylesheet" type="text/css" href="defaultpage.css"> </HEAD> <BODY BGCOLOR="#F79C19" link="ffffff" alink="999999" vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0"> #if ($message) $message #end <BR> #foreach($value in $tracks) <b> Track: $value.name - Length: $value.length</b><BR> #end <br> <img src="/images/tune_big.gif" height="250" width=283></td></tr> </table> </BODY> </HTML> Listing 12.10 The displaytracks.vm template. 在这里,你的web开发者已经指出了你所需要的另外一个Collection对象,用于存储某张CD上所有的歌曲。这个Collection对象被命名为$tracks,它通过控制器组件放置在上下文里。 控制器代码 你目前已经拥有了模型和视图组件,现在你需要一个控制器组件来把这两个组件联系在一起。Listing 12.11显示了这个Velocity Servlets,它将完成这个工作。 import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import org.apache.velocity.Template; import org.apache.velocity.context.Context; import org.apache.velocity.servlet.VelocityServlet; import org.apache.velocity.exception.*; import javax.naming.*; import javax.ejb.*; import cd.*; import org.apache.velocity.app.Velocity; public class cdVelocityHandler extends VelocityServlet { private CDRecordHome cdHome = null; private TracksRecordHome tracksHome = null; protected Properties loadConfiguration(ServletConfig config ) throws IOException, FileNotFoundException { Properties p = new Properties(); String path = config.getServletContext().getRealPath("/"); if (path == null) { System.out.println(" Unable to get the current webapp root"); path = "/"; } p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, path ); return p; } public void init() throws ServletException { try { javax.naming.Context cmp = (javax.naming.Context) new InitialContext().lookup("java:comp/env/cmp"); cdHome = (CDRecordHome) cmp.lookup("CDRecordBean"); tracksHome = (TracksRecordHome) cmp.lookup("TracksRecordBean"); } catch (NamingException e) { e.printStackTrace(); } } public Template handleRequest( HttpServletRequest req, HttpServletResponse res, Context context ) { Template template = null; if (req.getParameter("submit").equals("new")) { try { if (cdHome == null) { context.put("message", "Sorry we had an error"); } else { int tracks = Integer.parseInt(req.getParameter("tracks")); CDRecord cd = cdHome.create(req.getParameter("title"), req.getParameter("artist"), tracks); if (cd != null) { context.put("thanks", "Thank you for the new CD<BR>"); } else { context.put("thanks", "We are sorry but your request failed<BR>"); } try { template = getTemplate("thanks.vm"); } catch( Exception e ) { e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } else if (req.getParameter("submit").equals("obtain")) { try { if (cdHome == null) { context.put("message", "Sorry we had an error"); } else { Collection cds = cdHome.findByArtist(req.getParameter("artist")); context.put ("cds", cds); try { template = getTemplate("displaycds.vm"); } catch( Exception e ) { e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } else if (req.getParameter("submit").equals("tracks")) { try { if (tracksHome == null) { context.put("message", "Sorry we had an error"); } else { int id = Integer.parseInt(req.getParameter("id")); Collection tracks = tracksHome.findByCdID(id); context.put ("tracks", tracks); try { template = getTemplate("displaytracks.vm"); } catch( Exception e ) { System.out.println("Error " + e); } } } catch(Exception e) { e.printStackTrace(); } } else if (req.getParameter("submit").equals("addtrack")) { try { if (tracksHome == null) { context.put("message", "Sorry we had an error"); } else { int id= Integer.parseInt(req.getParameter("id")); TracksRecord track = tracksHome.create(id, req.getParameter("name"), req.getParameter("length")); if (track!= null) { context.put("thanks", "Thank you for the new track<BR>"); } else { context.put("thanks", "We are sorry but your request failed<BR>"); } try { template = getTemplate("thanks.vm"); } catch( Exception e ) { e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } else { } return template; } } Listing 12.11 The controller servlet for the CD example 这个控制器组件是一个Velocity Servlets,它使用VelocityServlet类的handleRequest()方法。在这里有一些让Servlets访问Velocity模板和实体EJB的步骤。 Velocity模板被放置在Servlets应用程序的根目录,这个目录是Servlets必须的。为了确定Servlets能够访问这个应用程序的根目录,需要为FILE_RESOURCE_LOADER_PATH属性设置真实的目录。你可以在loadConfiguration()方法里完成这个设置操作,这个方法将在handleRequest()方法调用之前被自动调用。 接着,你需要获得实体EJB home接口的访问权,你可以在init()方法里完成这个任务,init()方法将Servlets第一次执行时被调用,这个方法将为comp/env/cmp JNDI引用获得一个naming.Context上下文对象。这个引用将用于在系统里访问EJB。接下来,你需要查找每一个bean,并从这两个bean中返回适当的home对象。这些home对象将用于构建实体bean。 在handleRequest()方法里的代码用于检查用户请求的是这四个操作的哪一个操作(增加CD、搜索特定艺术家、查看某CD的全部歌曲、为CD增加歌曲)。让我分别看一下这四个操作: 增加新CD 当用户为一张新的CD输入标题、艺术家和该CD的所有歌曲时,代码必须能够完全把这些数据放到数据库表里。代码首先确定home接口对象是有效的,如果该接口无效,一个错误信息将会指派给“message”引用,该引用将增加到上下文中。 如果home接口对象是有效的,则从HTML的<form>传递过来的歌曲字符串被转换成integer。接着,歌曲和从<form>传递过来的值的剩余部分一同被传递给CD bean的home接口的create()方法。调用该方法的结果有可能是null,也可能是一个用于呈现CD表行的新的实体EJB。如果该方法的值不是null,那么“thanks”引用被指向一个文本字符串,同时被加入到上下文对象中。否则,一个失败的信息将会加入到上下文中。 在任意一种情况下,模板对象都会被设置成从getTemplate()方法(使用thanks.vm文件名参数)返回的值。如果没有异常发生,这个新模板对象会从handleRequest()方法返回,同时用户会看到适当的输出。 增加新CD歌曲(Track) 增加新CD歌曲的代码基本上和增加新CD的代码一样,只是接口不同,在此用的是track接口。在一个生产型(production)系统里,你必须确定歌曲应该加入到的CD表,以保证CD和歌曲能够正确关联。 通过艺术家搜索CD 通过特定的艺术家来获取CD列表和增加CD只有一小点的不同就是获取CD列表需要查询数据库。你应该记得这两个实体EJB的EJB文件里都定义了一个<query>元素。在CD数据表的情况下,我们定义的查询将返回特定艺术家所有的CD表行。 代码通过findByArtist(String)方法来调用这个查询。从这个方法返回的值是一个Collection对象,它包含了从CD数据表返回的零到若干个实体对象。不管有多少个对象在collection里,我们将使用下面的命令把这个Collection加入到上下文中 context.put ("cds", cds); 当Collection对象被加入到上下文中后,displaycds.vm模板就从服务器的磁盘取出,之后,这个模板对象被显示给用户。 列出CD歌曲 当用户想显示特定CD的歌曲时,代码将获取从<form>里的ID变量传递过来的ID,以及将这个ID传递给findByCdID(int)方法。这个方法执行TracksRecordBean的EJB文件里的<query>元素,该方法的结果是一个Collection对象,它包含了这张特定CD的所有歌曲。这个Collection对象通过$tracks引用被加入到上下文中。 高级Servlet功能 作为VelocityServlet基础类,许多附加的方法可以被重载。这些方法如下: ■ Properties loadConfiguration(ServletConfig)—一个允许附加的属性被加入到Servlets的属性中的方法。这些目前被定义到Velocity运行时的属性是: l static java.lang.String--COUNTER_INITIAL_VALUE—初始化#foreach指令的计数器值 l static java.lang.String--COUNTER_NAME—初始化#foreach指令的计数器名称 l static java.lang.String--DEBUG_PREFIX—日志信息前缀 l static java.lang.String--DEFAULT_RUNTIME_DIRECTIVES—默认运行时指令 l static java.lang.String--DEFAULT_RUNTIME_PROPERTIES—默认运行时属性 l static java.lang.String--ENCODING_DEFAULT—默认编码类型 l static java.lang.String--ERROR_PREFIX—错误信息前缀 l static java.lang.String--ERRORMSG_END—错误信息的结束标记,通过在#include指令里传递一个不允许参数时触发 l static java.lang.String--ERRORMSG_START—错误信息的开始标记,通过在#include指令里传递一个不允许参数时触发 l static java.lang.String--FILE_RESOURCE_LOADER_CACHE—在FileResourceLoader里打开一个公用的缓存 l static java.lang.String--FILE_RESOURCE_LOADER_PATH—在FileResourceLoader里设置一个公用的路径 l static java.lang.String--INFO_PREFIX—消息通知前缀 l static java.lang.String--INPUT_ENCODING—模板编码集 l static java.lang.String--INTERPOLATE_STRINGLITERALS—字符串篡改开关 l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_BUFFER_SIZE--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_FROM--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_SERVER--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_SUBJECT--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_TO--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_FILE_BACKUPS--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_FILE_SIZE--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_PATTERN--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_REMOTE_HOST--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_REMOTE_PORT--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_SYSLOGD_FACILITY--log4J配置 l static java.lang.String--LOGSYSTEM_LOG4J_SYSLOGD_HOST--log4J配置 l static int--NUMBER_OF_PARSERS—你想创建的解析器数量 l static java.lang.String--OUTPUT_ENCODING—输出流编码集 l static java.lang.String--PARSE_DIRECTIVE_MAXDEPTH—#parse指令允许的最大递归深度 l static java.lang.String--PARSER_POOL_SIZE—解析器在池里的总数 l static java.lang.String--RESOURCE_LOADER—用于重新找回资源加载器名称的关键字 l static java.lang.String--RESOURCE_MANAGER_CACHE_CLASS—实现资源管理缓存的类 l static java.lang.String--RESOURCE_MANAGER_CLASS—实现资源管理器的类 l static java.lang.String--RESOURCE_MANAGER_LOGWHENFOUND—用于确定是否对找到资源进行日志 l static java.lang.String--RUNTIME_LOG—定位Velocity日志文件 l static java.lang.String--RUNTIME_LOG_ERROR_STACKTRACE—堆栈跟踪错误信息输出 l static java.lang.String--RUNTIME_LOG_INFO_STACKTRACE—堆栈跟踪报告信息输出 l static java.lang.String--RUNTIME_LOG_LOGSYSTEM—用于指定外面的日志系统 l static java.lang.String--RUNTIME_LOG_LOGSYSTEM_CLASS—你想要使用的日志系统类 l static java.lang.String--RUNTIME_LOG_REFERENCE_LOG_INVALID—非法引用日志 l static java.lang.String--RUNTIME_LOG_WARN_STACKTRACE—堆栈跟踪警告信息输出 l static java.lang.String--UNKNOWN_PREFIX—未知信息前缀 l static java.lang.String--VM_CONTEXT_LOCALSCOPE—VM本地作用域开关,默认为false l static java.lang.String--VM_LIBRARY—本地Velocimacro库模板名称 l static java.lang.String--VM_LIBRARY_AUTORELOAD—一个自动加载VM资源库的开关,用于开发阶段 l static java.lang.String--VM_MESSAGES_ON—VM消息开关,默认为true l static java.lang.String--VM_PERM_ALLOW_INLINE—布尔值,默认为true,既是否允许内联(在模板里)宏定义 l static java.lang.String--VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL—布尔值,默认为false,即是否允许内联(在模板里)宏定义替换现有的定义 l static java.lang.String--VM_PERM_INLINE_LOCAL—是否强迫内联宏为本地的,默认为false l static java.lang.String--WARN_PREFIX—警告信息前缀 ■ Context createContext(HttpServletRequest, HttpServletResponse)—该方法允许开发者创建他们自己的上下文对象,可以被用作私有merge()方法。 ■ void setContentType( HttpServletRequest,HttpServletResponse)—默认情况下,handleRequest()方法将输出HTML格式的文本,但是,你可以更改成其他格式,比如XML,甚至是图片文件。 ■ void mergeTemplate(Template, Context, HttpServletResponse)—如果你想自己控制输出来代替handleRequest()方法,那么你可以使用createContext()方法得到你自己的上下文,并且把它们和模板进行合并,之后通过handleRequest()方法输出给response对象。mergeTemplate()方法得到所有的三个对象,并且产生输出。 ■ void requestCleanup(HttpServletRequest, HttpServletResponse, Context)—如果你想自己处理输出,你应该重载requestCleanup()方法,来处理任何最后的结果。默认情况下,这个方法并没有实现。 ■ protected void error(HttpServletRequest, HttpServletResponse, Exception)—当在处理用户请求发生一个异常时,该方法将被调用。你可以重载这个方法来提供更多高级的错误处理能力。默认实现只是发送一个错误信息和一个堆栈跟踪信息给用户。 增加报表(report) 我们的应用程序在此已经把注意力集中到产生一个HTML窗体格式的输出。但是,如果你不想要任何HTML样式的表,而是只想得到一个基于文本格式的所有数据库里CD的报表该怎么办?Ok,现在让我们考虑一下Listing 12.12里的Velocity模板。
Listing 12.12里的Velocity模板用于输出一个页面的头部,最多列出50张数据库里的CD,之后,产生另一个页面头部。在两个头部之间的CD数量可以改变,以适应于不同的输出。为了产生的完整的CD报表,你需要在主CD index.html页面增加一个按钮。下面给出这个按钮的代码: <h3>Reports</h3> <form action="http://localhost:8080/cd/cdVelocityHandler" method="post"> <input type="submit" name="submit" value="fullreport"> - download 'report.txt' to your local system </form> 这个新窗体在index页面上显示了一个名叫FullReport的按钮。当用户单击这个按钮的时候,控件被传递到cdVelocityHandler servlet(在Listing 12.11里定义的)。Listing 12.13的代码将用于处理这个新按钮。 else if (req.getParameter("submit").equals("fullreport")) { try { if (cdHome == null) { context.put("message", "Sorry we had an error"); } else { Collection cds = cdHome.findAllCDs(); context.put ("cds", cds); try { template = getTemplate("fullreport.vm"); } catch( Exception e ) { e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } Listing 12.13 The control servlet report task. 除了配合CDRecordBean实体bean的findAllCDs查询外,这些代码没有什么特别的地方。新报表的查询见Listing 12.14。 <query> <query-method> <method-name>findAllCDs</method-name> </query-method> <ejb-ql>SELECT o FROM CDTable o</ejb-ql> </query> Listing 12.14 The bean query. 新查询从CDTable数据库表从提出所有的行。一旦所有的行被提出,结果Collection对象就会被放入上下文中。最后,Velocity模板fullreport.vm被调用,用于输出这个查询的结果。输出的结果见Figure 12.5
正如你所看见的一样,这个输出并不像我们希望的一样,只输出文本样式,而不需要HTML进行润色。如果我们想生成一个文本格式的文件并可以进行下载该怎么做?让我们来考虑一下Listing 12.15里的模板。
Listing 12.15里Velocity模板使用tab占位符来控制从数据库生成数据的输出格式。这也就意味着,Artis和Title对应的字符串将拥有一个适当的格式和对齐方式,同时不再顾及这些字符串的长度。你肯定不想看到任何参差不齐的行排列样式。为了完成这个任务,在知道Artist的值后,你必须生成一个正确的tab个数。如果你看一下模板代码,你将会看到下面这行: $value.artist$stringlength.tabs($value.artist)$value.title 这一个行是由下面三行代码构成的: $value.artist $stringlength.tabs($value.artist) $value.title $value.artist和$value.title命令只简单为当前行产生将要显示的artist和title数据。这个命令里最有趣的部分是$stringlength.tabs($value.artist)。这个$stringlength引用和一个放置在上下文的对象(使用StringLength类)关联。StringLength类代码见Listing 12.16 public class StringLength { public StringLength(){} public String tabs(String st) { String s = new String(); for (int i=3;i>st.length()/5;i--) s = s + ""t"; return s; } } Listing 12.16 The StringLength class. StringLength类只有一个简单的任务:暴露一个名叫tabs()的方法。这个方法的作用是计算并返回一个确定个数的tabs字符串,以用于排列artist和title的值。艺术家的名称被传递给tabs()方法,之后确定数量的tabs被返回。 现在,你的Velocity模板已经能够返回正确的输出,OK,让我们考虑一下如何让你的控件Servlets生成一个文件,以便用户下载。Listing 12.17的代码显示了Full-Report按钮的控件Servlets代码。 else if (req.getParameter("submit").equals("fullreport")) { try { if (cdHome == null) { context.put("message", "Sorry we had an error"); } else { Collection cds = cdHome.findAllCDs(); context.put ("cds", cds); try { res.setContentType("APPLICATION/OCTET-STREAM"); res.setHeader("Content-Disposition","attachment; filename=report.txt"); template = getTemplate("fullreport.vm"); } catch( Exception e ) { e.printStackTrace(); } } } catch(Exception e) { e.printStackTrace(); } } Listing 12.17 The servlet code for downloading the report. 正如你所看到的一样,findAllCDs查询被用于从数据库中提取所有的CD信息,同时,模板fullreport.vm被用于输出。其中增加了两个新命令: res.setContentType("APPLICATION/OCTET-STREAM"); res.setHeader("Content-Disposition","attachment; filename=report.txt"); 这两个命令用于告诉用户浏览器,从<form>返回的信息是一个名叫report.txt的文件,这个信息将导致出现一个下载对话框,用于用户下载文件。Figure 12.6显示了使用这个应用程序产生的文件下载情况。
本章小节和下章介绍 在这一章里,我们介绍了一种可能对开发者有用的混合Velocity和Servlets的技术。在下一章里,我们将演示如何扩展Velocity驱动的站点,以适当国际化需要的技术。 |
|
来自: 木木的阳光 > 《Velocity》