如果你对apache cocoon有了解或者以涉足,你可能对在cocoon中如何使用java更好
的实现特定的逻辑有所疑惑。这篇文章将给您介绍如何使用xsp(extensible server page)
和action。同时还有示例和设计原则。
你可能听到了一些来自apache的关于cocoon的声音。现在,在经历了三年的发展后,
cocoon已经逐渐的从一个简单的实现xsl (extensible stylesheet language) 转换
的servlet成长为一个饱满的web应用框架。
cocoon是用java开发的,一般做为servlet运行在象tomcat这样的servlet容器中。
在这篇文章里,我们将介绍到两种办法来在基于cocoon的应用中使用java实现商业逻辑。
首先,让我们来总的了解一下cocoon。
cocoon正式的定义是一个xml发布引擎,我们可以理解cocoon为一个用来产生、转换、处理
和输出数据的框架。也可以理解cocoon是一个从多种数据源接收数据再应用不同的处理,最
后将数据以想要的格式输出的机器。
我们也可以定义cocoon是一个数据流机器。也就是说,当你使用cocoon时,你定义数据的路
径或者流程来产生web应用的页面。
下面是cocoon主要的一些基本的原理:
1、cocoon把所有的数据做为sax (simple api for xml) 事件来处理,任何非xml的数据都要
转变成xml描述。
2、生成器(能生成sax事件)的组件负责处理输入数据
3、序列化器负责处理输出数据,将数据输出到客户端(浏览器、文件等)。
4、开发人员组合生成器、序列化器和其它组件构成管道。所有的管道都在一个叫做站点地图的
文件中定义。
5、通过uri (uniform resource identifier)来匹配管道,但uri是与物理资源脱离的。
第5点需要说明一下:
对于传统的web server,uri一般映射到物理资源。
例如,这个uri http://localhost/index.html 在apache server将映射到一个叫index.html的
html文件。
在cocoon中,uris 和 物理资源可以是没有任何绝对的相互关系的。你可以自由的设计uri来帮
助用户更好的浏览你的站点。最后,你可以更好的组织你的文件让其容易管理和维护。
为了更好的了解cocoon的处理模型,可以看一个简单的管道。
下面这个例子定义了一个叫index.html的页面。这个管道位于叫sitemap.xmap站点地图中:
<map:match pattern="index.html">
<map:generate type="file" src="content/mainfile.xml"/>
<map:transform type="xslt" src="content/stylesheets/mainstyle.xsl"/>
<map:serialize type="html"/>
</map:match>
这个管道有三步:
首先是一个生成器组件filegenerator从xml文件"content/mainfile.xml "读取数据。
(filegenerator实际上已经提前在地图中定义,可以通过"type"属性来引用。cocoon中所有的
管道组件都是通过它们的type属性来引用的。)
接着进行转换,转换器traxtransformer将xsl stylesheet应用到引入的数据。
最后,序列化器htmlserializer将数据写到客户端的浏览器。
你可能疑惑,上面所说的和java开发有什么联系呢?
我们把cocoon的应用分成三个部分:
1、数据的收集层 data collection (generation)
2、数据的处理和转换层 data processing&transforming
3、数据的输出层 data output(serialization)
那么,java开发在cocoon的处理转换层是非常重要的。cocoon的转换和处理层是基于cocoon的
应用的核心,通过这一层对输入数据的处理,逻辑的应用,你就可以得到所期望的输出。
在cocoon中,你可以有下面四种实现逻辑的方法:
1、使用转换器(transformer)组件:他们按照你给定的规则转换传入的数据。典型的例子
便是traxtransformer。
2、通过不同的 request、session、uri来选择不同的组件做出正确的处理。
3、使用已有的或者自己实现的action。
4、使用混合了java代码和内容的xsp。
这篇文章介绍最后两种办法:xsp 和 action。xsp 和 action的开发都是在servlet context内。
确切的说,两种组件(实际上是所有的组件)都要存取request, response, session, 和
context对象。在某些方面,你要实现的大量的逻辑都会与这些对象相互作用。
xsp
xsp是cocoon项目的创新。你可以把它和jsp相比较,因为它们都是混合逻辑和内容而且jsp的
taglib和xsp的logicsheet也很相似。
xsp位于管道的起点,实际上它被cocoon转换成生成器(generator)来给管道中其余的组件提
供数据。
让我们看下面这个叫 sample1.xsp 简单的示例:
<?xml version="1.0"?>
<xsp:page language="java" xmlns:xsp="http://apache.org/xsp">
<xsp:logic>
date now = new date();
string msg = "boo!";
</xsp:logic>
<content>
<title>welcome to cocoon</title>
<paragraph>
this is an xsp. you can see how we it contain both logic
(inside the <xsp:logic> tags) and content. in the logic block
above, we created a date object whose value is <xsp:expr>now</xsp:expr>.
oh, we also had a special message for you: <xsp:expr>msg</xsp:expr>
</paragraph>
</content>
</xsp:page>
首先注意这个文档的根标记是<xsp:page>。
<xsp:page language="java" xmlns:xsp="http://apache.org/xsp">
这个标记定义xsp的language(可以是java或者javascript)和用到的逻辑单的namespace。
接着是我们定义了两个java变量的<xsp:logic>块。
这些<xsp:logic>块可以有多个,可以出现在你希望的任何地方,而且可以包含各种java代码。
最后,是我们自己的内容,从用户自己的跟标签开始,在上面的示例中是<content>。在内容部分里,
我们可以用<xsp:expr>这个标签得到在前面定义的变量。
记住,一个xsp实际上就是一个生成器generator。cocoon将其转换成java源文件然后编译、执行它。
(如果想看xsp转换成的java源文件,到你的servlet容器的工作路径下去找。例如,
如果你使用tomcat 4.0.4,那么路径就是下面这样:
$catalina_home/work/standalone/localhost/cocoon/cocoon-files/org/apache/cocoon/www.)
xsp执行后产生的xml数据被传递给管道其余的组件。
看下面这个管道实例:
<map:pipeline match="*.xsp">
<map:generate type="serverpages" src="examples/{1}.xsp"/>
<map:serialize type="xml"/>
</map:pipeline>
这里,我们使用一个指定的生成器 serverpagesgenerator,来处理我们简单的xsp。返回给客户
端未加修饰的xml。
注意例子中使用了特别的 {1} 变量引用:它代替值在管道开始处的通配符指示的值。也就是说,
如果我们在浏览器中打开我们的web应用中的sample1.xsp,那么 {1}的值便是sample1。
记住,同多数cocoon组件一样,xsp访问request, response, session, 和 context 对象。这些
对象实际上是httpservletrequest, httpservletresponse, httpsession, 和
httpservletcontext的封装,cocoon正式版本提供了大量的存取这些对象的方法。
xsp在从数据库读取数据的时候特别有用。
数据库数据自然地以行和列组织,所以数据库数据很容易转换到xml。然而,jdbc
(java database connectivity)没有适合地代码完成向xml的转换。
xsp可以让我们在读取数据时很容易,这要感谢esql 逻辑单。esql 逻辑单除了隐藏了详细
的jdbc代码,还允许将行和列放入到特定的标签中。同时esql 逻辑单也可以执行嵌套查询
和执行更新命令。
下面,我们举个xsp应用的例子:
假如我们想将一些cocoon的资源(名称和url)存储到数据库。
首先,我们定义存放资源的数据表,然后当用户通过关键字搜索时,我们使用xsp来找到相应
的行,将数据显示给用户。
随后,我们构建一个表单来增加新的列。
表的定义和插入的数据如下面所示。我们这里使用的数据库是mysql,如果您使用的是其它的
数据库,要注意做相应的改动。这个例子中,必须要有配置好数据库连接池。
表结构如下:
use test;
create table resources (
resourceurl varchar(255) not null,
resourcename varchar(64) not null
);
插入一些资源数据:
insert into resources values
(http://xml.apache.org/cocoon, cocoon home page);
insert into resources values
(http://www.galatea.com/flashguides/cocoon-tips-2.xml, cocoon 2.0 tips and tricks);
表建好后并且cocoon也正确的配置过后,我们就可以写下面这个xsp例子:
<?xml version="1.0"?>
<xsp:page language="java"
xmlns:xsp="http://apache.org/xsp"
xmlns:esql="http://apache.org/cocoon/sql/v2">
<xsp:logic>
string keyword = request.getparameter("value");
</xsp:logic>
<content>
<title>search results</title>
<esql:connection>
<esql:pool>resources</esql:pool>
<esql:execute-query>
<esql:query>
select * from resources
where resourcename like %<xsp:expr>keyword</xsp:expr>%
</esql:query>
<esql:results>
<resources>
<esql:row-results>
<resource>
<esql:get-columns/>
</resource>
</esql:row-results>
</ resources >
</esql:results>
</esql:execute-query>
</esql:connection>
</content>
</xsp:page>
注意在<xsp:page>标签中声明的名称空间(namespace)。任何时候,在xsp中使用逻辑单的时候,
必须要声明其名称空间(namespace)。你可以在cocoon webapp路径下的web-inf/cocoon.xconf找
到逻辑单的定义。xsp 名称空间的声明就是要说明这是个xsp逻辑单。
实际上,所有的xsp至少要实现xsp逻辑单。在xsp被转换成java源文件之前,其中的逻辑单
(实际上仅是xsl文件)会先做xslt处理转换成java代码。因此,在上面的例子中的所有的
esql标签都会转换成了我们所熟悉的jdbc代码。但是并不是所有的标签都可以变成jdbc代码,
注意上面示例中的<esql:pool>块,它涉及到了在web-inf/cocoon.xconf文件中定义的数据库
连接池。上面程序中使用的连接池叫做"resources" ,当然你可以使用你所喜欢的定义。
注意,我们这里使用<resources> 这个标签将结果集包了起来而且每行的数据都放到<resource>标
签里。这样我们就可以很容易的编写样式表来将xml转换成其它浏览器可以理解的格式。我们没有
为表的列定义任何标签,通过使用<esql:get-columns/>,cocoon会将每一列的值放到自动以相应
的列名定义的标签里面。
现在,让我注意一下例子中的sql查询语句,正如你所看到的,这条sql是动态生成的。当用户
通过gets 或者 posts提交数据到这个xsp后,在xsp的顶部,我们将request参数的值赋给
了keyword变量,然后根据keyword组成sql语句。
既然这个例子很简单,让我们把它变复杂一点,加入email功能,可以在用户提供email地址后,
将查询结果发送给用户。
xsp示例如下:
<?xml version="1.0"?>
<xsp:page language="java"
xmlns:xsp="http://apache.org/xsp"
xmlns:esql="http://apache.org/cocoon/sql/v2"
xmlns:sendmail="http://apache.org/cocoon/sendmail/1.0"
xmlns:xsp-request="http://apache.org/xsp/request/2.0"
>
<content>
<xsp:logic>
string keyword = <xsp-request:get-parameter name="value"/>;
string emailaddr = <xsp-request:get-parameter name="email"/>;
string emailbody = "";
</xsp:logic>
<title>search results</title>
<esql:connection>
<esql:pool>resources</esql:pool>
<esql:execute-query>
<esql:query>
select * from resources where resourcename like
%<xsp:expr>keyword</xsp:expr>% order by resourcename
</esql:query>
<esql:results>
<resources>
<esql:row-results>
<resource>
<xsp:logic>
emailbody += <esql:get-string column="resourcename"/>;
emailbody += ", " + <esql:get-string column="resourceurl"/> + "\n";
</xsp:logic>
<esql:get-columns/>
</resource>
</esql:row-results>
</resources>
</esql:results>
</esql:execute-query>
</esql:connection>
<xsp:logic>
if (emailaddr != null) {
<sendmail:send-mail>
<sendmail:charset>iso-8859-1</sendmail:charset>
<sendmail:smtphost>my_smtp_host</sendmail:smtphost>
<sendmail:from>my_from_address</sendmail:from>
<sendmail:to><xsp:expr>emailaddr</xsp:expr></sendmail:to>
<sendmail:subject>cocoon search results</sendmail:subject>
<sendmail:body><xsp:expr>emailbody</xsp:expr></sendmail:body>
</sendmail:send-mail>
}
</xsp:logic>
</content>
</xsp:page>
来自sendmail逻辑单的几个标签让我们拥有了发送email的能力。在这个例子中,我们将查询结果
的每一行相加赋值给emailbody变量做为邮件的正文。当用户通过request参数提供一个email地址,
我们就可以发送email了。当然这需要您提前设定好smtp服务器和from地址。
cocoon知道根据sendmail逻辑单来处理在sendmail名称空间里的标签,因为这个名称空间已经
在<xsp:page>标签中已经声明。查看示例中的声明,你会看到xsp-request这个名称空间。
xsp-request逻辑单提供了request常用方法的封装。虽然在xsp中直接访问request对象和
使用xsp-request逻辑单没有什么功能上的区别,但是,理论上使用logicsheet的标签比直
接的java代码更优美。
在运行这个例子之前,你必须要先在cocoon.xconf文件中设置sendmail逻辑单,cocoon的配置
文件都在web application 的 web-inf目录下。用你熟悉的编辑器打开cocoon.xconf文件,
找到<target-language name="java">标签。在这个块内,你会发现所有其它逻辑单的定义。
在最后一个逻辑单(soap逻辑单)后加入下面的内容:
<builtin-logicsheet>
<parameter name="prefix" value="mail"/>
<parameter name="uri" value="http://apache.org/cocoon/sendmail/1.0"/>
<parameter name="href"
value="resource://org/apache/cocoon/components/language/markup/xsp/java/sendmail.xsl"/>
</builtin-logicsheet>
这个定义将http://apache.org.cocoon/sendmail/1.0名称空间和已经包括在cocoon jar中
的sendmail.xsl样式表联合起来。
要使用sendmail逻辑单的功能,cocoon必须要 mail.jar 和 activation.jar这两个jar。
如果您使用的server是tomcat4.x的话,那么它们位于$catalina_home/common/lib。
actions
action功能很强大,你可以将它放到管道的任何地方。action可以认为是小的自包含的机器,
它获取某些输入数据,做一些处理,然后返回hashmap对象。不同于cocoon中的generators,
transformers, serializers组件,action不对实际的xml内容做任何事情,它主要在管道中
实现一些逻辑。
学习action包括要对管道参数做些了解,有时管道的组件必须交流数据。当然,xml内容会通
过sax事件传递;但是,我们所说的是管道组件本身的功能需要的值。
管道参数有两种:input 和 output。input参数是由紧跟在组件声明后面的一个或者多个
<map:parameter>标签来定义的。它们为组件提供一个或者多个值来影响其操作。
matcher和action这两个组件可以为它们后面的组件提供能存取的output变量。
这些output参数放在hashmap对象里,可以通过key名称(如:{1})来引用。
所有的管道都至少有一个由管道开始处的matcher提供的hashmap。我们在管道中使用这个
hashmap对象,使用{1}可以取得hashmap中key为1的值。
cocoon本身包含一些内置的action。其中有一个是依靠数据库来鉴别用户的action。当我们
想保护cocoon中的某些页面,只允许授权的用户访问时,可以将用户的id和密码存储到数据
库里,然后使用databaseauthenticationaction来做登录确认。
这个databaseauthenticationaction要求我们提供一个xml描述文件来说明要使用哪个表和哪
些列。下面是这个描述文件的示例:
<?xml version="1.0" encoding="utf-8"?>
<auth-descriptor>
<connection>resources</connection>
<table name="users">
<select dbcol="user_name" request-param="userid" to-session="userid"/>
<select dbcol="user_password" request-param="userpwd"/>
</table>
</auth-descriptor>
上面这个文件说明用户认证action将使用resources连接池和user表,request提交的userid参数
和userpwd参数将与user表中的user_name和user_password列来比较,如果成功确认,将参
数userid写到session里。
当你在管道中使用一个action时,它必须先在站点地图中的<map:components>块中的
<map:actions>块中定义。如下:
<map:components>
<!– all other component definitions go here –>
<map:actions>
<map:action name="authenticator"
src="org.apache.cocoon.acting.databaseauthenticatoraction"/>
<!– other actions definitions go here –>
</map:actions>
</map:components>
一旦定义过后,就可以使用这个action来负责我们要保护的区域。下面为要保护的区域定
义了三个管道:
<map:match pattern="protected/login.html">
<map:read mime-type="text/html" src="secret/login.html"/>
</map:match>
<map:match pattern="protected/login">
<map:act type="authenticator">
<map:parameter name="descriptor" value=" secret/auth-info.xml"/>
<map:redirect-to uri="index.html"/>
</map:act>
<map:redirect-to uri="login.html"/>
</map:match>
<map:match pattern="protected/*">
<map:match type="sessionstate" pattern="*">
<map:parameter name="attribute-name" value="userid"/>
<map:match pattern="protected/*.html">
<map:read mime-type="text/html" src=" secret/*.html"/>
</map:match>
<map:match pattern="protected/*.xsp">
<map:generate type="serverpages" src=" secret/{1}.xsp"/>
<map:serialize type="xml"/>
</map:match>
</map:match>
<map:redirect-to uri="login.html"/>
</map:match>
第一个管道简单的提供了一个登录的表单,是个html文件,不需要转换。第二个管道处理
从login.html提交的实际的登录动作。第三个来处理我们要保护的内容。
下面我们做详细的论述:
databaseauthenticationaction依靠描述文件来验证登录。但是,我们如何知道验证是否
成功呢?对于action,如果它们返回了有效的hashmap,那么在<map:act>块里的部分将执
行。如果返回null值,那么块下面的部分将执行。也就是说,按照上面管道的定义,我们
有两种可能的结果,即:如果认证通过,我们就可以到达受保护的区域,如果失败将返回
到login页面。
在 protected/* 管道有几个嵌套的matcher,第二个的type是sessionstate,这个matcher
实际上是wildcardsessionattributematcher,在这里用来读取session里的userid的值。
在这个例子中,我们知道databaseauthenticationaction设置了一个叫userid的session属
性,我们通过检测userid属性来判断用户是否登录成功,如果它不存在,则转向到login页面。
在cocoon已经有一个databaseaddaction可用来插入数据,但为了更好的理解action,我们
将写一个自己的action用来将新的resource记录插入到resources表中。
我们假想你已经编写了一个html页面,可用来post两个变量name和url到管道。我们的
action将从request对象中找回name和url参数,将其插入到表中,最后返回一个hashmap对象。
下面是程序代码:
package test;
import org.apache.avalon.excalibur.datasource.datasourcecomponent;
import org.apache.avalon.framework.component.componentexception;
import org.apache.avalon.framework.component.componentmanager;
import org.apache.avalon.framework.component.componentselector;
import org.apache.avalon.framework.parameters.parameters;
import org.apache.avalon.framework.thread.threadsafe;
import org.apache.avalon.framework.component.composable;
import org.apache.avalon.framework.activity.disposable;
import org.apache.cocoon.environment.objectmodelhelper;
import org.apache.cocoon.environment.redirector;
import org.apache.cocoon.environment.request;
import org.apache.cocoon.environment.session;
import org.apache.cocoon.environment.sourceresolver;
import org.apache.cocoon.acting.abstractaction;
import java.sql.connection;
import java.sql.statement;
import java.sql.sqlexception;
import java.util.hashmap;
import java.util.map;
public class addresourceaction extends abstractaction
implements threadsafe, composable, disposable
{
protected componentselector dbselector;
protected componentmanager manager;
public void compose(componentmanager manager) throws componentexception {
this.dbselector =
(componentselector) manager.lookup(datasourcecomponent.role + "selector");
}
protected final datasourcecomponent getdatasource(string pool)
throws componentexception {
return (datasourcecomponent) this.dbselector.select(pool);
}
public map act( redirector redirector, sourceresolver resolver,
map objectmodel, string
source, parameters param )
throws exception
{
request req = objectmodelhelper.getrequest(objectmodel);
session ses = req.getsession(true);
string poolname = param.getparameter("pool");
string resourcename = req.getparameter("name");
string resourceurl = req.getparameter("url");
if (poolname == null) {
getlogger().error("missing a pool name");
return null;
}
if (resourcename == null || resourceurl == null) {
getlogger().error("missing input parameters");
return null;
}
map map = new hashmap();
datasourcecomponent datasource = getdatasource(poolname);
connection conn = null;
boolean status = false;
try {
conn = datasource.getconnection();
statement stmt = conn.createstatement();
string cmd = "insert into resources values (" +
resourcename + ", " +
resourceurl + ")";
stmt.executeupdate(cmd);
map.put("resource-name", resourcename);
map.put("resource-url", resourceurl);
getlogger().debug("resources insert completed by user " +
ses.getid());
status = true;
stmt.close();
} catch (exception e) {
getlogger().error("stmt failed: ", e);
} finally {
try {
if (conn != null) conn.close();
} catch (sqlexception sqe) {
getlogger().warn("error closing the datasource", sqe);
}
}
if (!status) return null;
return(map);
}
public void dispose() {
this.manager.release(dbselector);
}
}
这儿有大量的东西需要消化,特别是如果你不熟悉cocoon的结构。我们一步步的来说明。
首先,cocoon action的主方法是 act(),当在管道中使用action时cocoon将调用这个方法。
在这个示例中,act()得到request参数、从连接池中得到数据库连接,执行插入,然后填充
hashmap对象,并将其返回。
在act方法的开始是从objectmodelhelper组件中取得request对象,然后得到两个参数。这个
action需要另外一个参数,pool;它将告诉我们使用哪个连接池。如果这个参数没有,那么
action将返回null而且将错误写到日志里。有了pool的名称,我们就可以从连接池得到数据
库的连接。avalon的excalibur组件用来负责cocoon的连接池。如果你不熟悉avalon,可以
访问这里http://jakarta.apache.org/avalon/ 。
代码中的insert statement是直接的jdbc语法。在插入成功后,会将成功的信息写到日志里。
对于日志,如果按上面action的写法,所有的日志信息都写到你的web application的
web-inf/logs/sitemap.log文件。
最后,我们将两个输入参数写到了map对象,虽然它们都在request对象中,这样做是多余的,
但我们这样做是为了示例map对象的用法。
看一下这个action在站点地图中的定义。我们必须首先在站点地图的<map:components>区定
义这个action。
<map:components>
<!– all other component definitions go here –>
<map:actions>
<map:action name="authenticator"
src="org.apache.cocoon.acting.databaseauthenticatoraction"/>
<map:action name="add-resource" src="test.addresourceaction"
logger="sitemap.action.addresourceaction"/>
<!– other actions definitions go here –>
</map:actions>
</map:components>
在管道中使用这个action:
<map:match pattern="addresource">
<map:act type="add-resource">
<map:parameter name="pool" value="resources"/>
<map:read mime-type="text/html" src="examples/confirmation.html"/>
</map:act>
<map:read mime-type="text/html" src="examples/addresource.html"/>
</map:match>
可以看到,在<map:act> 行的下面,紧跟着的<map:parameter>标签为action提供"pool"参数。
一切顺利的话,action将返回一个map对象,confirmation页面将被显示。
在浏览器中打开http://localhost:8080/cocoon/addresource,你会看到一个输入表单。
表单提交后,如果插入成功,将显示confirmation页面,如果出现错误,将再次返回到表单
页面。查看web-inf/logs/sitemap.log,错误信息会告诉你出现了什么错误。
如何有效的使用xsp和action?
xsp和action是在cocoon中实现逻辑的两种不同的办法。选择哪一种更适合呢?
xsp在取数据或者创建结构化的数据方面是很有用的。action被证明在控制数据流程
(并不产生或者影响数据)的逻辑实现上很有用。我们上面看到的用户验证和数据库
操作便是这样的两个例子。
然而,有一点需要说明的问题:xsp会将逻辑和内容混合。而cocoon的一个基本的原则
就是逻辑、内容、表示的分离。
在使用xsp的时候,我们提出以下几点建议:
首先,尽可能的使用逻辑单,逻辑单会很好的将java代码隐藏。
第二,尽量使用cocoon的提供的功能,如:在做数据库的select的时候,我们也可以用
sqltransformer来实现。
第三,在决策方面的逻辑尽可能的使用selector, matcher或action组件。
最后,当无法避免在你的xsp中插入java逻辑的话,尽可能的让<xsp:logic>小,而且不要
把它们散布到各种你的标签中。
