2002-09-17 09:57
用Perl和XML轻松开发多种界面的Web服务
简介
与Web服务有关的的一个基本问题是如何创建一个既能够通过基于浏览器的客户端又能够通过编程方式让客户端自动访问应用程序。在本文中,我们将讨论如何利用Perl和XML简单地创建多界面的Web服务。
我们之所以选择Perl和XML,与SOAP、XML-RPC和REST的优缺点无关,也不是为了试图解决哪种工具更适合用来开发Web服务的问题。我们在这里想要说明的是,只要动点脑筋并使用一些Perl模块,就可以创建出实用的而且能够通过多种客户端进行访问的Web服务。
例子━━WebSemDiff:多界面的XML Semantic Diff Web服务
在本篇文章中,我们将建立XML::SemanticDiff模块的一个Web界面。XML::SemanticDiff能够在忽略细节的情况下比较二个XML文档的内容。
阅读本文之前,我建议读者应当对CGI::XMLApplication有个基本的了解。对它有一定的了解会对理解我们本篇文章的内容有所帮助。事实上只要了解一个典型的包括三个部分的CGI::XMLApplication应用程序就够了:连接客户端和应用程序的CGI脚本、处理任务的Perl模块以及将Perl模块中返回的DOM树转换为客户端应用程序能够接受的XSLT样式表。
理解CGI::XMLApplication的基本架构是十分重要的,因为SOAP::Lite模块也使用了相同的架构,开发多客户端访问应用的根本目的在于对这二个模块整合的理解。
首先,我们来看看CGI::XMLApplication和SOAP::Lite用来比较上传到服务器的文件所使用的基本模块:
package WebSemDiff;
use strict;
use
CGI::XMLApplication;
use XML::SemanticDiff;
use
XML::LibXML::SAX::Builder;
use XML::Generator::PerlData;
use vars qw( @ISA );
@ISA = qw( CGI::XMLApplication
);
在导入必要的模块以及声明软件包与CGI::XMLApplication的继承关系后,我们需要实现使浏览器界面工作的方法。
浏览器界面有二种状态:缺省状态是提醒用户上传二个XML文档进行比较,显示比较结果的结果状态(或在比较时出现的错误)。selectStylesheet()方法返回由应用程序生成的DOM树转换成的样式表的路径。在这里我们不对semdiff_default.xsl和semdiff_result.xsl这二个样式表进行详细的讨论。
sub selectStylesheet {
my ( $self, $context ) = @_;
my
$style = $context->{style} || 'default';
my $style_path =
'/www/site/stylesheets/';
return $style_path . 'semdiff_' . $style .
'.xsl';
}
缺省情况下,必需的getDOM()方法将返回一个XML::LibXML::Document对象。在向浏览器返回结果前,由selectStylesheet()方法设定的XSLT样式表将对该文档对象进行转换。
sub getDOM {
my ( $self, $context ) = @_;
return
$context->{domtree};
}
getXSLParameter()方法提供了从类向样式表传送值的一种方式(可以通过<xsl:param>元素获得该值)。在这里,我们只增加所有的请求参数,让样式表来选择相关的域。
sub getXSLParameter {
my $self = shift;
return
$self->Vars;
}
由于缺省状态只是一个不要求应用程序逻辑或特别处理的简单提示,因此我们只需实现对结果状态的访问即可:
# 登录事件和回调事件
sub registerEvents {
return qw( semdiff_result
);
}
sub event_semdiff_result {
my ( $self, $context ) = @_;
my
( $file1, $file2, $error );
my $fh1 = $self->upload('file1');
my $fh3 =
$self->upload('file2');
$context->{style} = 'result';
在为应用程序的状态设置合适的样式后,我们就能够获得包含有上传的XML文档的文件句柄。我们首先检查二个句柄是否存在,如果存在,则转换为二个简单的标量:
if ( defined( $fh1 ) and defined( $fh3 ) ) {
local $/ =
undef;
$file1 = <$fh1>
$file2 = <$fh3>;
其次,我们创建包含由通过调用compare_as_dom()方法生成的比较结果的DOM树。将这次调用封装在一个eval块中,以确保我们能够获得在处理上传的文档时发生的解析错误。在稍后,我们将仔细地研究 compare_as_dom()和dom_from_data()方法。
eval {
$context->{domtree} = $self->compare_as_dom( $file1,
$file2 );
};
if ( $@ ) {
$error = $@;
}
}
else {
$error = 'You
must select two XML files to compare
and wait for them to finish
uploading';
}
if ( $error ) {
$context->{domtree} = $self->dom_from_data( {
error => $error } );
}
如果二个文档完全相同,compare_as_dom()返回一个示定义的字符。如果没有返回DOM对象,也没有错误产生,我们创建一个只包含告诉用户二个文档相同的一个<message>元素的文档。
unless ( defined( $context->{domtree} )) {
my $msg = "Files
are semantically identical.";
$context->{domtree} = $self->dom_from_data( {
message => $msg } );
}
}
在完成信号收集事件后,我们就可以继续编写信号收集事件和SOAP调度程序共享的核心方法了。
首先,我们需要来创建compare()方法。它不仅仅是同名的XML::SemanticDiff的方法的容器,它还接受二个包含被比较的XML文档的句柄并返回结果。
sub compare {
my $self = shift;
my ( $xmlstring1,
$xmlstring2 ) = @_;
my $diff = XML::SemanticDiff->new( keeplinenums => 1
);
my @results = $diff->compare( $xmlstring1, $xmlstring2 );
return
\@results;
}
dom_from_data()方法通过XML::Generator::PerlData对任何公用Perl数据结构的引用进行处理创建一个XML::LibXML::Document对象(DOM树形式的XML文档),并将生成器与XML::LibXML::SAX::Builder连接生成DOM树。还记得吗,我们在结果事件回调中调用了该方法来创建包含有适当信息的DOM树。
sub dom_from_data {
my ( $self, $ref ) = @_;
my $builder =
XML::LibXML::SAX::Builder->new();
my $generator =
XML::Generator::PerlData->new( Handler => $builder );
my $dom =
$generator->parse( $ref );
return $dom;
}
最后,我们将创建compare_as_dom()方法。它也是最后的二个方法的容器,它以DOM树的形式返回二个文档的比较。
sub compare_as_dom {
my $self = shift;
my $diff_messages =
$self->compare( @_ );
return undef unless scalar( @{$diff_messages} ) >
0;
return $self->dom_from_data( { difference => $diff_messages }
);
}
1;
在创建了上面的方法后,我们就仅需要创建提供能够供各种客户端应用程序访问的CGI脚本了,这也是需要综合利用CGI::XMLApplication和SOAP::Lite 的地方。
#!/usr/bin/perl -w
use strict;
use
SOAP::Transport::HTTP;
use WebSemDiff;
if ( defined( $ENV{'HTTP_SOAPACTION'} ))
{
SOAP::Transport::HTTP::CGI
-> dispatch_to('WebSemDiff')
->
handle;
}
else {
my $app = WebSemDiff->new();
$app->run();
}
SOAP::Lite的dispatch_to()方法连接SOAP与一特定的模块(或模块的目录)。在本例中,它使我们能够重用实现浏览器界面的WebSemDiff类,模块的共享意味着CGI只不过是一个请求代理,它提供了对基于连接客户端应用应用程序类的方法的访问。通过互联网浏览器访问应用程序的用户被提示上传二个XML文档,并通过compare_as_dom()方法获取结果,SOAP客户端只可以直接访问compare_as_dom、更低级的compare()等方法。
至此,我们已经开发了一个能够运行的应用程序。下面我们就来用一些客户端与它进行连接,比较二个文档,并返回相应的结果。
为了简明起见,我们将使被比较文档尽量简单。第一个文档的名字为doc1.xml:
<?xml version="1.0"?>
<root>
<el1 el1attr="good"/>
<el2
el2attr="good">Some Text</el2>
<el3/>
</root>
第二个XML文档的名字为:doc2.xml :
<?xml version="1.0"?>
<root>
<el1 el1attr="bad"/>
<el2
bogus="true"/>
<el4>Rogue</el4>
</root>
从浏览器进行访问
对/cgi-bin/semdiff.cgi的请求将提示用户上传二个文档:
图1
在对文件进行比较后,结果如下:
图2
从SOAP客户端访问
SOAP::Lite既有服务器也有客户端实现。在这里我们将使用它创建一个连接我们的应用程序的SOAP界面的客户端应用程序。为了节约篇幅,我们将跳过与变量处理、打开和读取要比较的XML文档相关的客户端脚本,而重点讨论与SOAP相关的部分:
#!/usr/bin/perl -w
use strict;
use
SOAP::Lite;
...
my $soap = SOAP::Lite
->
uri('http://my.host.tld/WebSemDiff')
->
proxy('http://my.host.tld/cgi-bin/semdiff.cgi')
-> on_fault(
\&fatal_error );
my $result = $soap->compare( $file1, $file2 )->result;
print "Comparing $f1 and $f2...\n";
if ( defined $result and scalar( @{$result} ) == 0 ) {
print
"Files are semantically identical\n";
exit;
}
foreach my $diff ( @{$result} ) {
print $diff->{context} . '
' .
$diff->{startline} . ' - ' .
$diff->{endline} . ' '
.
$diff->{message} .
"\n";
}
将我们的二个XML文档的路径传递给该脚本代码会产生下面的结果:
Comparing docs/doc1.xml and docs/doc2.xml...
/root[1]/el1[1]
3 - 3 Attribute 'el1attr' has different value in element
'el1'.
/root[1]/el2[1] 4 - 4 Character differences in element
'el2'.
/root[1]/el2[1] 4 - 4 Attribute 'el2attr' missing from element
'el2'.
/root[1]/el2[1] 4 - 4 Rogue attribute 'bogus' in element
'el2'.
/root[1] 5 - 5 Child element 'el3' missing from element
'/root[1]'.
/root[1] 5 - 5 Rogue element 'el4' in element '/root[1]'.
另外,我们可以使用SOAP::Lite的自动调度机制来提高代码的可读性:
use SOAP::Lite +autodispatch =>
uri =>
'http://my.host.tld/WebSemDiff',
proxy
=>'http://my.host.tld/cgi-bin/semdiff.cgi',
on_fault => \&fatal_error
;
my $result = SOAP->compare( $file1, $file2 );
print "Comparing $f1 and $f2...\n";
# etc ..
从RESTful客户端进行访问
REST架构的爱好者会非常喜欢我们的应用程序能够提供访问未经转换的XML文档。
#!/usr/bin/perl -w
use strict;
use
HTTP::Request::Common;
use LWP::UserAgent;
my ( $f1, $f2 ) = @ARGV;
usage() unless defined $f1 and -f $f1
and defined $f2 and -f
$f2;
my $ua = LWP::UserAgent->new;
my $uri = "http://my.host.tld/cgi-bin/semdiff.cgi";
my $req = HTTP::Request::Common::POST( $uri,
Content_Type
=> 'form-data',
Content => [
file1 => [ $f1 ],
file2 => [ $f2
],
passthru => 1,
semdiff_result => 1,
]
);
my $result =
$ua->request( $req );
if ( $result->is_success ) {
print
$result->content;
}
else {
warn "Request Failure: " . $result->message
. "\n";
}
sub usage {
die "Usage:\nperl $0 file1.xml file2.xml
\n";
}
该脚本(restful_semdiff.pl)能够将下面的XML文档输出到STDOUT:
<?xml version="1.0"
encoding="UTF-8"?>
<document>
<difference>
<context>/root[1]/el1[1]</context>
<message>
Attribute
'el1attr' has different
value in element
'el1'.
</message>
<startline>3</startline>
<endline>3</endline>
</difference>
<difference>
<context>/root[1]/el2[1]</context>
<message>
Character
differences in element
'el2'.
</message>
<startline>4</startline>
<endline>4</endline>
</difference>
...
</document>
结论
在本文中我们完全没有提到XML-RPC,原因有二个:
第一,SOAP::Lite提供的XML-RPC客户端和服务器端界面与SOAP使用的非常相似,因此使用它意义不大。
第二,与SOAP客户端不同的是,XML-RPC客户端没有与它们的请求相关联的标准和明确的HTTP头部,这意味着我们的CGI请求代理必须采取一定的措施来区分XML-RPC客户端和正常的互联网浏览器。通过对POST请求和“text/xml”的内容类型进行检查,探测XML-RPC请求是可能的,但这种方案是“不健壮的”。
通过本篇文章的介绍,我衷心地希望读者能够掌握结合利用SOAP::Lite和CGI::XMLApplication创建简洁、模块化的支持通过SOAP、REST和HTML浏览器进行访问的应用程序的方法。