簡介
Perl的普及與互聯網的蓬勃發展有直接的關系。Perl強大的功能和容易擴充的特性使得它成為開發CGI應用最自然的選擇,並由此迅速地成為CGI腳本的首選語言。CGI本身並非十全十美。但由於得到了眾多開發商的青睐,CGI的應用至今仍然十分廣泛,而且沒有跡象表明在近期會“退休”。
典型的CGI::XMLApplication腳本包括三部分:一個很小的提供對該應用程序訪問支持的可執行腳本、實現各種管理者方法的邏輯模塊、根據應用狀態可能有一個或多個XSLT樣式表,XSLT樣式表能夠將模塊返回的結果轉化成浏覽器可以向用戶顯示的格式。
下面我們通過例子來簡要地介紹CGI::XMLApplication的應用。
例1:CGI XSLT網關
CGI::XMLApplication假定,參與一個項目的設計和開發人員使用XSLT樣式表分離應用的邏輯和表示,這樣可以使這種分離顯得非常直接,也不會對項目帶來影響。開發人員只要能夠使setStylesheet返回符合當前應用狀態的XSLT樣式表的位置即可。應用建立的DOM樹的轉換、XSLT參數向轉換引擎的傳遞、轉換後內容向浏覽器的傳輸對用戶而言都是透明的。
為了重點說明這種分離,我們的第一個例子不是傳統意義上的Web應用,而是一個通用的XSLT網關,它可以添加到服務器的CGI-bin中,將整個XML內容的目錄樹轉化為符合請求的浏覽器的格式,而這一切對於用戶、樣式表和文檔的作者而言也都是透明的。
第一步是建立連接客戶端的請求和應用的CGI腳本。我們希望XML文檔能夠方便地通過URL浏覽,並使創建這些文檔間的超鏈接非常直觀。因此,我們將創建一個沒有擴展名的CGI腳本,以便將它作為URL路徑中的一個節點,節點右邊的所有內容將在包含XML內容的虛擬文檔環境中進行解釋。在這種情況下,我們將CGI稱作是樣式表選擇者。
use strict; use lib '/path/to/secure/webapp/libs'; use XSLGateway; use CGI qw(:standard);my $q = CGI->new(); my %context = (); my $gateway_name = 'stylechooser';
在加載合適的模塊和設置一些在整個腳本范圍內有效的變量後,我們開始向被傳遞給處理該應用邏輯的類的%context中添加一些域。在這個應用軟件中,我們只傳輸要求的指向腳本文件路徑右邊的URL(REQUEST條目)和包含有存儲在查詢參數style中的數據的STYLE關健字。
$context{REQUEST} = $q->url(-path => 1); $context{REQUEST} =~ s/^$gateway_name\/?//; $context{REQUEST} ||= 'index.XML'; $context{STYLE} = $q->param('style') if $q->param('style');
最後,我們創建了XSLGateway邏輯類的一個實例,並通過調用其run方法處理請求,將%context作為唯一的參數。
my $app = XSLGateway->new(); $app->run(%context);
CGI腳本就完成了。下面我們創建完成大部分工作的XSLGateway模塊:
package XSLGateway; use strict; use vars qw(@ISA); use CGI::XMLApplication; use XML::LibXML; @ISA = qw(CGI::XMLApplication);
象我在簡介中提到的那樣,CGI::XMLApplication通過事件調用起作用:應用程序類中一個給定的方法的執行依賴於一個指定域的輸入(一般情況下是用來提交表格的按鈕的名字。),必須執行二種調用方法:selectStylesheet和requestDOM方法。
selectStylesheet返回有關的XSLT樣式表的全文件系統路徑。為了簡單起見,我們假定樣式表將保存在一個單一的目錄中。我們可以通過$context->{STYLE}域提供其他的樣式表,從而增加系統的靈活性。
sub selectStylesheet { my $self = shift; my $context = shift; my $style = $context->{STYLE} || 'default'; my $style_path = '/opt/www/htdocs/stylesheets/'; return $style_path . $style . '.xsl'; }
下一步,我們需要創建requestDOM方法,該方法將返回被傳輸的XML文檔的XML::LibXML DOM表達式。由於我們的網關只適用於靜態文件,我們需要使用XML::LibXML對文檔進行解析,並返回結果樹。
sub requestDOM { my $self = shift; my $context = shift; my $xml_file = $context->{REQUEST} || 'index.xml'; my $doc_path = '/opt/www/htdocs/xmldocs/'; my $requested_doc = $doc_path . $xml_file; my $parser = XML::LibXML->new; my $doc = $parser->parse_file($requested_doc); return $doc; }
至此,我們的CGI腳本已經可以安全地在服務器的cgi-bin目錄中安全地運行了,並在一些適當的目錄中上載一些XML文檔和一個或二個XSLT樣式表。下面我們就可以開始檢驗我們的工作成果了。對http://localhost/CGI-bin/stylechooser/mydocs/somefile.xml的請求將會使互聯網服務器從/opt/www/htdocs/xmldocs/目錄中選取mydocs/somefile.XML文件,使用/opt/www/htdocs/stylesheets/中的樣式表default.xsl對該文件進行轉換,並將它傳輸給客戶。
如果需要,我們可以擴充這一基本的框架,例如,可以在樣式表選擇CGI腳本程序添加一些查找組件,選擇合適的樣式表,可以設置或讀取HTTP cookIEs,對網站進行修飾。
例2:一個簡單的購物系統
在該例子中,我們將使用CGI::XMLApplication創建一個簡化的Web應用程序,購物系統。
與上個例子相同,這個應用程序中與CGI-BIN有關的部分仍然非常地少。我們所需要作的只不過是初始化CustomerOrder應用類並調用它的run()方法。這次,我們將CGI.pm中Vars作為%context的PARAMS域:
use strict; use CGI qw(:standard); use lib '/path/to/secure/webapp/libs'; use CustomerOrder; my $q = CGI->new(); my %context = (); $context{PARAMS} = $q->Vars; my $app = CustomerOrder->new(); $app->run(%context);
在這個例子中,我們假定該應用中的產品信息存儲在關系數據庫中,產品清單不是太長,使我們在應用中不會出現多屏才能顯示相關信息的麻煩:用戶輸入訂購的產品數量的主要數據輸入屏,顯示訂購單內容和所選物品總價格的確認屏,顯示訂單已經處理的提示。為了簡單起見,我們在這裡沒有涉及送貨和財務數據的輸入等問題。
package CustomerOrder; use strict; use vars qw(@ISA); use CGI::XMLApplication; use XML::LibXML::SAX::Builder; use XML::Generator::DBI; use DBI; @ISA = qw(CGI::XMLApplication);
在加載必要的模塊和定義從CGI::XMLAplication中繼承的類後,我們開始創建應用中與各種狀態有關的事件調用。首先,我們必須通過創建registerEvents()方法注冊這些事件。在本例中,我們將注冊order_confirm 和order_send方法,這二個方法設置%context中的SCREENSTYLE域。稍後,我們將利用該屬性定義在顯示客戶端的數據時應該使用三個XSLT樣式表中的哪一個。
需要注意的是,這些事件將被映射到實現它們的實際的子程序中,子程序的命名規則是event_<事件名>,例如,order_confim事件是由event_order_confim執行的。另外,還需要注意的是,各種事件的選擇是由CGI::XMLApplication根據其查找一個與注冊事件同名的表格參數的能力進行的。例如,要執行order_confirm事件,表格組件中必須包含一個提交非空值的名字為order_confirm的表格域。
# 事件的注冊和事件調用
sub registerEvents { return qw( order_confirm order_send ); } sub event_order_confirm { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_confirm.xsl'; } sub event_order_send { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_send.xsl'; }
如果沒有請求執行其他的事件,則缺省地執行event_default。在本例中,我們只使用它將SCREENSTYLE域設定為一個合適的值。
sub event_default { my ($self, $context) = @_; $context->{SCREENSTYLE} = 'order_default.xsl'; }
每次請求都會執行event_init方法,而且總是在其他方法之前執行它,這使得它非常適合對應用中被其他事件使用的部分進行初始化。在本例中,我們使用它返回利用fetch_recordset()方法從數據庫中獲取的產品信息的、最初的DOM樹。
sub event_init { my ($self, $context) = @_; $context->{DOMTREE} = $self->fetch_recordset(); }
state-handler方法完成後,我們需要執行必需的selectStylesheet和requestDOM方法。
與在第一個例子中一樣,我們假設所有的應用的樣式表都存儲在服務器上相同的目錄中。我們所需要作的是返回$context->{SCREENSTYLE}的值所指定的路線,並添加到末尾。
# app config and helpers sub selectStylesheet { my ($self, $context) = @_; my $style = $context->{SCREENSTYLE}; my $style_path = '/opt/www/htdocs/stylesheets/cart/'; return $style_path . $style; }
在研究requestDOM處理程序之前,我們先來詳細地研究fetch_recordset helper方法。
需要記住的是,我們要做的工作是從一個關系數據庫中選擇所訂購產品的有關信息,但傳遞給XSLT處理器的數據必須是DOM樹。在本例中,我們不通過編程的方法,而是利用XML::Generator::DBI,它能夠從執行SQL SELECT語句得到的數據中生成SAX數據。創建要求的DOM樹就是建立XML::LibXML::SAX::Builder(它從SAX事件中創建XML::LibXML DOM樹)的實例。
sub fetch_recordset { my $self = shift; my $sql = 'select id, name, price from products'; my $dbh = DBI->connect('dbi:Oracle:webclIEnts', 'chico', 'sWordfish') || dIE "database connection couldn't be initialized: $DBI::errstr \n"; my $builder = XML::LibXML::SAX::Builder->new(); my $gen = XML::Generator::DBI->new(Handler => $builder, dbh => $dbh, RootElement => 'document', QueryElement => 'productlist', RowElement => 'product'); my $dom = $gen->execute($sql) || dIE "Error Building DOM Tree\n"; return $dom;}
fetch_recordset方法完成了另一項很重要的任務,但它返回的DOM樹只包含我們想向客戶發送信息的一部分,我們還必須獲取用戶輸入的產品數量,另外,還需要提供一個訂購產品的總計。
sub requestDOM { my ($self, $context) = @_; my $root = $context->{DOMTREE}->getDocumentElement(); my $grand_total = '0';
為了將當前的訂貨數量作為更大的文檔的一部分,我們將遍歷所有的產品元素,並在每行中添加和子元素。的值可以從$context->{PARAMS}域獲得。
foreach my $row ($root->findnodes('/document/productlist/product')) { my $id = $row->findvalue('id'); my $cost = $row->findvalue('price'); my $quantity = $context->{PARAMS}->{$id} || '0'; my $item_total = $quantity * $cost; $grand_total += $item_total; # add the order quantity and item totals to the tree. $row->appendTextChild('quantity', $quantity); $row->appendTextChild('item-total', $item_total); }
最後,我們將增加一些有關訂單的元信息,方法是在具有元素的根元素中添加一個元素,該元素中包含有當前所選貨物的總價值。
$grand_total ||= '0.00'; my $info = XML::LibXML::Element->new('instance-info'); $info->appendTextChild('order-total', $grand_total); $root->appendChild($info); return $context->{DOMTREE}; }
細心的讀者可能已經注意到,我們這個非常簡單的應用程序在order_send方法中沒有作任何實際的事。決定如何處理這些數據是產品訂購應用程序中與具體的購物網站最有關的部分。
結束語
CGI::XMLApplication在CGI腳本程序的編程中提供了一種清晰的、模塊化的隔離系統的內容和表示的方法,單就這一點,就值得我們對它進行一番研究。此外,它還可以使我們避免糾纏於一些細節問題,而集中精力解決主要的問題。