<tutorialcontent><b>充分利用現有的 EJB 使行動人力資源變得強大</b> <br><br><a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#sec7">Aashish Patil</a> (<a href="mailto:ash01@vsnl.net">ash01@vsnl.net</a>)<br>理科碩士研究生,計算機科學系,Southern California 大學<br>2001 年 7 月<br><br><blockquote>如果您的企業已經依賴於使用多層的企業 Java 應用,那麼您也許比想象中更接近無線科技的未來。透過描述一個範例應用,Aashish Patil 向您展示了如何用最少的人力物力把您現有的企業 Java 基礎元件連線到無線網路中去。使用現有的 EJB、修改過的 Servlet 以及新的 WML 和 WMLScript 頁面會使這個過程產生一個飛躍。</blockquote>無線應用協定(Wireless Application Protocol,WAP)可以提高一個企業現有 Web 體系結構的價值。如果您已經使用了企業 Java 應用,您可以容易地將它們與 WAP 服務整合,這樣可以為行動人力資源帶來有用的資料和功能。在這篇文章裡,我會涉及到使用與 WAP 相關的 J2EE 的基本要素,然後建立一個 WAP∕企業 Java 範例應用,以展示您如何把自己的 EJB 連線到無線網路中去。 <p> </p> <h3><a name="sec1">背景︰J2EE 和 WAP</a></h3>在閱讀本文前,您應該對 Java 2 平台,Enterprise Edition(J2EE)體系結構有個基本的瞭解。您可以透過下面的參考資料章節找到關於 J2EE 的更多訊息的連結。作為一個回顧,下面是一張為桌上型客戶端設計的典型 J2EE 應用的示意圖。 <br><br><b>圖 1 ─ J2EE 應用結構</b><br><img height="318" alt="J2EE 應用結構" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig1.gif" width="556"><p></p> <p>在圖 1 中,包括 JavaServer Page(JSP)和 Servlet 的那一層負責生成動態 HTML 頁面。而在 WAP 應用中,這一層將生成動態的無線標記語言(Wireless Markup Language,簡稱 WML)頁面。因此,為了轉換一個標準的 J2EE 應用使之為行動裝置所使用,您將不得不編寫新的 JSP,並且在某些情況下,還要編寫新的 Servlet。企業 JavaBean(EJB)保持不變,因 為它們與資料表現無關。 </p> <table cellspacing="0" cellpadding="5" width="30%" align="right" border="1"><tbody><tr><td background="/developerWorks/images/tutorial/wireless/j2me/wapapplication/bg-gold.gif"> <b>什麼是 WML?</b> <br>正如 Web 瀏覽器顯示 HTML 編碼的資料一樣,支援無線標記語言(WAP)的裝置顯示 WML 編碼的資料;另外,正如 Web 工作人員使用 JavaScript 把腳本功能嵌入到 Web 頁面一樣,裝置工作人員使用 WMLScript 把同樣的功能嵌入到 WML 頁面中。WML 是 XML 的一個子集,而對於精通 HTML 或其它標示語言的人來說它看起來很眼熟。WML 有一個獨一無二的特性需要牢記︰它像<i>一盒卡片</i>;一個單一的 HTML 文件顯示成一個單一的 Web 文件,而一個單一的 WML 文件可以包括很多卡片。WAP 裝置的螢幕一次只能顯示一張卡片。關於一些 WML 和 WMLScript 的連結可以參閱下面的<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#sec6">參考資料</a>章節。</td></tr></tbody></table> <p>有些人認為 Servlet 無需變更,或者說︰祇要把 Servlet 的輸出簡單地重新導向到生成動態 WML 頁面的 JSP 上就已經足夠了。然而,Servlet 不能區別從桌上型和從 WAP 裝置發來的請求;既然 WAP 應用可能無法實作基於 Web 的體系結構的所有功能,所以在這方面並沒有混淆的地方,這一點很重要。也正由於此,工作人員通常為 WAP 應用設計新的 Servlet。然而在大多數情況下,這些 Servlet 與那些在基於 Web 的體系結構上提供類似功能的 Servlet 非常相似。 <br><br>在圖 1 中沒有出現但對 WAP 應用又很重要的另一個元件是 WAP 閘道。這個元件負責 WAP 堆疊和 Internet 堆疊之間的相互轉換。 <br><br>圖 2 是圖 1 的改進版,顯示了使用 WAP 裝置作為用戶端的 J2EE 應用的結構︰ <br><br><b>圖 2 ─ WAP∕J2EE 應用的結構</b><br><img height="176" alt="WAP∕J2EE 應用的結構" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig2.gif" width="489"><br><br>按照圖示,所有自 WAP 用戶端到 Web 伺服器的請求必須透過 WAP 閘道傳送。儘管 WAP 閘道也可以作為放置 WML∕WMLScript 頁面的 WAP 伺服器,但使用 Web 伺服器來放置這些頁面更為方便。 <br><br>有很多 WAP 閘道的部署方法。對於多數 WAP 應用來說,閘道或由 ISP 部署,或由提供這個應用的公司來部署。後者更為安全,我們以後會解釋;然而,如果使用者要求在他們的 WAP 裝置上進行多用途的網路存取,一個內部的 WAP 閘道會很不方便。大多數非 ISP 不希望他們的閘道被用來存取他們自己網站以外的其它網站;因此,為了存取其它網站,使用者將不得不使用 ISP 閘道。但對於被 WAP 用戶端使用的每一個閘道來說,使用者都必須定義一個不同的連線,正如 Windows 98 的撥號網路一樣 ─ 而且在每個裝置上,這樣連線的數目通常是有限的。這就增加了使用者的不便性,並且在存取一個網站時造成 WAP 裝置中的連線阻塞。 </p> <p> </p> <h3><a name="sec2">WAP 應用設計的考慮事項</a></h3>當使用 WAP 時,一個習慣為桌上型客戶端編寫 J2EE 應用的工作人員會遇到一些新的挑戰。以下是在建置 WAP 應用時您也許會碰到的一些問題。 <br><br><i>我可以在螢幕上顯示幾行訊息?</i><br>事實上,對顯示多少行沒有特別限制,祇要不超過面板的最大尺寸就行(隨裝置的不同而不同)。然而,為了避免太多捲動,每格螢幕(即卡片)5 至 7 行最佳。 <br><br><i>我應該考慮哪些安全問題?</i><br>一些電話不支援使用 POST 方法傳送表單資料。因此,使用者名稱和密碼必須透過 GET 方法傳送。在 WAP 閘道上,如果日誌功能被活化並且請求已被記錄,管理員就有能看到使用者名稱和密碼。如果閘道是由 ISP 或其它第三方提供的,這個問題就會特別突出。 <br><br>即使一個安全的連線也不能完全消除安全隱患。那些傳送到 WAP 閘道的資料使用 WTLS(Wireless Transport Layer Security)加密,它使用與標準 TLS 相同的算法。然而,傳送到 WAP 閘道的資料是二進位的編碼格式(對 WAP),所以這些加密後的資料必須用 TLS 解密和再加密以適用於網際網路。經過一段時間以後,敏感資料在 WAP 閘道上以明文的形式出現。駭客則會在適當的時刻,將記憶體中的訊息轉儲出來,進而成功地存取這些敏感資料。 <br><br>按照註釋,解決該問題的一種辦法是在自己公司(而不是在 ISP)設一個 WAP 閘道。在這種情況下,讓一個可信的人可以操縱閘道,並且可以關閉日誌功能。 <br><br>您也可以用 WMLScript 來編寫自訂的加密算法,以對用戶端的使用者名稱和密碼進行加密。這祇有在使用簡單的算法時才有可能實作;在支援 DES 類的算法上,WMLScript 不夠強大。 <br><br><i>我怎樣保持 Session?</i><br>WAP 用戶端不支援 Cookie。這樣,當使用者在您的網站的不同頁面之間穿梭時,為了在伺服器端保留關於用戶端的訊息,在向伺服器傳送每個請求的同時,一個 Session ID 必須被當作參數傳遞。Session ID 的參數名根據 Servlet 引擎的不同而不同。 <br><br>有時,預設的 Session ID 長度很大幅度地增加了每個請求的長度。結果導致用戶端或 WAP 閘道可能將此請求看作一個無效的 URL 而拒絕。這樣有必要縮短 Session ID 的長度。請檢視一下您正在使用的 Servlet 引擎的說明文件中關於 Session ID 參數名的部分。如果您碰到過無效 URL 的錯誤,這個說明文件也應提供有關縮短 Session ID 值長度的指南。 <p></p> <p> </p> <h3><a name="sec3">建立起範例應用</a></h3>XYZ Ltd. 是一家生產 PDA,可佩戴式的電腦,及其它普及計算裝置的公司。公司的銷售人員拜訪客戶,提供 XYZ 產品的現場展示;某些展示要求銷售人員必須跑很遠的路去客戶那裡。那麼在路上,他們是怎麼收到客戶清單和其它重要資料的呢? <br><br>為此使用電子郵件將會需要體積較大且昂貴的可攜式電腦或永無止境地網咖的搜尋;在用戶端使用傳真機則更不切實際。取而代之的是 XYZ 的銷售人員會透過支援 WAP 功能的裝置接收資料,例如手機或 PDA。使用行動裝置,銷售人員能在拜訪客戶時向公司提供及時的回饋。公司就能馬上安排給客戶及時發送貨物並維護目前的銷售統計訊息。 <br><br>我們的應用有兩個主要目標。首先,我們流動的銷售人員應該能使用它在 WAP 裝置上檢視客戶清單。第二,如果一個客戶希望買貨,那麼銷售人員應能使用裝置來下訂單。此外,任何 WAP 應用的一個重要目標應該是減少使用者必要的按鍵數目。由於受手持裝置的使用者界面限制,使用者需要輸入的資料量應控制在最少。 <br><br>這是一張顯示我們系統的體系結構的流程示意圖 <br><br><b>圖 3 ─ 應用流程示意圖</b><br><img height="623" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig3.gif" width="431"><br><br>使用者首先必須登入存取系統;然後他們能瀏覽客戶清單和每個客戶的詳細資訊。如果他們希望為某一特定的客戶下訂單,那麼系統會提供他們一個產品清單,他們可以從中為該客戶選擇一個特定的產品。 <br><br>在本文剩下的大多數內容中,我們會討論實作該應用的 Servlet 和 JSP 程式碼,並會考察 JSP 和 Servlet 一起工作的方式。關於每個 JavaServer Page 的討論還附有圖解,顯示了 JSP 在裝置螢幕上的輸出。 <br><br>清單 1,<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#login">Login.jsp </a>接受使用者名稱和密碼,並把它們作為參數來呼叫 LoginServlet。對這個和其它所有的 JSP 來說,MIME 類型都應被設定成 text/vnd.wap.wml 類型。在傳遞請求的同時,上面的 Login.jsp 還傳遞了一個叫 SessionID 的參數。它必須與每個傳送到伺服器的請求一起傳遞。參數名 SessionID 是一個預留位置;請參考應用伺服器的說明文件,找到適用於您特定的應用伺服器的正確的參數名。Java 方法 HttpServletResponse.encodeURL(String URL) 自動加入 Session ID;在我們的應用裡,這已經被廣泛地使用在 Servelet 中。 <p></p> <p> </p> <h3><a name="login">Login.jsp</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> <%@ page contentType="text/vnd.wap.wml" %> <?xml version="1.0"?> <!--<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> --> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN" "http://www.wapforum.org/DTD/wml12.dtd"> <wml> <card id="login" title="Login Screen"> <p align="center"> <br/> Login Name: <input name="LogonName" type="text" emptyok="false" value="" size="20"/> <br/> Password: <input name="LogonWord" type="password" emptyok="false" value="" size="20"/> </p> <do type="prev" label="Next"> <go href="/LoginServlet" method="get"> <postfield name="username" value="$(LogonName)"/> <postfield name="password" value="$(LogonWord)"/> <postfield name="SessionID" value="<%=session.getId()%>"/> </go> </do> </card> </wml> </code></pre></td></tr></tbody></table> <img height="334" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig4.gif" width="310"><br><br>驗證空白的輸入欄位時會出現問題。在 input 標示裡有一個屬性,它讓您使輸入欄位不為空︰ <br><br><table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> <input name="name" type="text" emptyok="false" size="20"/> </code></pre></td></tr></tbody></table> <br><br>一個手機使用者必須存取各個獨立的對話方塊螢幕去輸入資料。問題出現了,因為使用者寧願選擇直接存取下一盒或下一張卡片而不願透過對話方塊螢幕去輸入資料。一個使用者面對如圖 4 所示的螢幕時也許會遺漏密碼並敲下 NEXT。 <br><br>一個工作人員可以透過使用 WMLScript 的驗證來避免這個問題的發生(透過使用 onclick 事件)。然而,直到輸入一個值到輸入框以後,您傳遞到 WMLScript 函式的代表輸入欄位值的那個變數才開始被初始化。因此,若無密碼鍵入,傳遞到該函式的是未初始化的變數和腳本錯誤結果。這個問題的解決方法是在伺服器端驗證所有的輸入欄位。 <br><br>清單 2,<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#LoginServlet">LoginServlet</a> 是我們問題的解決方案︰它可以認證銷售人員,並把他記錄在系統中。它也可以在伺服器端為銷售人員建立一個 Session。程式碼塊上的註釋明瞭指出在哪裡這些作業會被執行。一旦成功登入,裝置顯示如清單 3 所示的主選單(<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#MainMenu">MainMenu.jsp</a>)。 <p></p> <p> </p> <h3><a name="LoginServlet">LoginServlet</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> import java.io.*; import javax.servlet.http.*; import javax.servlet.*; import javax.rmi.*; import javax.naming.*; import java.util.*; public class LoginServlet extends HttpServlet { /* The Salesman EJB is an entity ejb */ SalesmanHome home; Salesman salesman; public void init() throws ServletException { try { System.out.println("trying to get initial context"); Context ic = getInitialContext(); System.out.println("Got InitContext"); Object objRef = ic.lookup("Salesman"); System.out.println("Got obj ref"); home = (SalesmanHome) PortableRemoteObject.narrow(objRef,SalesmanHome.class); System.out.println("Got home"); }catch(Exception e) { System.out.println("Error in init"); e.printStackTrace(); } } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String name = null; String password = null; try { System.out.println("Within goGet"); /* The parameter names are 'username' and 'password' */ name = req.getParameter("username"); password = req.getParameter("password"); if((name == null) || (name.length() <= 0) || (password == 0) || (password.length() <=0 )) { res.sendRedirect(res.encodeURL("/Status.jsp?code=2")); } name = name.trim(); password = password.trim(); /* Try to see if a salesman by this 'name' exists. The finder method findByName of the Salesman EJB, does this. It returns a java.util.Collection object containing the results of the search. */ Collection users = home.findByName(name); if(users.size() <= 0) { /* No salesmen found by the specified name */ res.sendRedirect(res.encodeURL("/Status.jsp?code=3")); } Iterator i = users.iterator(); /* Iterate throught the search results of findByName. Check if the password of any results matches the password entered */ while (i.hasNext()) { Salesman salesman = (Salesman)i.next(); String tmpPassword = (String)salesman.getPassword(); if(password.equals(tmpPassword)) { /* Check if an old session exists. If it does then invalidate it and create a new one */ HttpSession sess = req.getSession(false); if(sess != null) // a null is returned if an old session does not exist { sess.invalidate(); //invalidate old session } sess = req.getSession(); //create a new session String id = (String)salesman.getPrimaryKey(); //get salesman's id /* At this point the salesman has been validated and his id obtained. Store these parameters in the session. */ sess.setAttribute("SalesmanId",id); sess.setAttribute("SalesmanName",name); res.sendRedirect(res.encodeURL("/MainMenu.jsp")); } } }catch(Exception npe) { if((name == null) || (name.length() <= 0) || (password == 0) || (password.length() <=0 )) { res.sendRedirect(res.encodeURL("/Status.jsp?code=2")); } else res.sendRedirect(res.encodeURL("/Status.jsp?code=1")); } } private Context getInitialContext() throws NamingException { Context initial = null; try { /* There are other methods of obtaining the initial context too. Check the J2EE docs and your application server docs for these. */ initial = new InitialContext(); }catch(Exception ne) { System.out.println("Unable to get an initial context"); } return initial; } } </code></pre></td></tr></tbody></table> <p></p> <p> </p> <h3><a name="MainMenu">MainMenu.jsp</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> <%@ page contentType="text/vnd.wap.wml" %> <?xml version="1.0"?> <!--<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> --> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.2//EN" "http://www.wapforum.org/DTD/wml12.dtd"> <wml> <card id="MainMenu" title="Main Menu"> <p align="center"> <a href="/ClientViewServlet" title="View Clients"> View Clients </a><br/><a href="/LogoutServlet" title="Logout"> Log Out </a></p> </card> </wml> </code></pre></td></tr></tbody></table> <p></p> <p>如圖 5 所示,該文件將兩個連結顯示在裝置螢幕上。其中第二個終止了目前的 Session;第一個指向目前的客戶清單。在目前版本的程式流程中,銷售人員必須在開始任何銷售交易之前先從清單中選擇一個客戶;有關銷售產品的清單只能在後繼的螢幕上顯示(後面將會討論到細節)。也有其它可能的程式流程︰舉例來說,用來直接將使用者送到產品清單的連結可以被加到主選單中。但是,您不應該在任一選單中提供太多連結,否則支援 WAP 的裝置的小螢幕會因此而變得混亂不堪。 <br><br><img height="334" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig5.gif" width="310"><br><br>在圖 5 的主選單中敲下 View Clients 將呼叫清單 4 中的 <a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#ClientViewServlet">ClientViewServlet</a>,它獲得銷售人員將要拜訪的客戶清單。程式碼上的註釋說明了 Servlet 怎樣從用戶端上找到該訊息。接著 Servlet 將清單放到 Session 物件中並呼叫 ClientList.jsp。(這裡和下一段中提到的 Session 物件是來自於 Java servlet 套件中的 HttpSession 類別。) </p> <p> </p> <h3><a name="ClientViewServlet">ClientViewServlet</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> import java.io.*; import javax.servlet.http.*; import javax.servlet.*; import javax.rmi.*; import javax.naming.*; import java.util.*; public class ClientViewServlet extends HttpServlet { /* This servlet interacts with the Client Entity EJB. The client EJB has a finder method, findBySalesman. This method accepts the salesman id and obtains all the clients whom this salesman is supposed to visit. */ ClientHome clientHome; public void init() throws ServletException { try { System.out.println("trying to get initial context"); Context ic = (InitialContext) getInitialContext(); System.out.println("Got InitContext"); Object objRef = ic.lookup("Client"); System.out.println("Got obj ref"); clientHome = (ClientHome) PortableRemoteObject.narrow(objRef,ClientHome.class); System.out.println("Got Home"); }catch(Exception e) { System.out.println("Error in init"); e.printStackTrace(); } } public void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException { try { HttpSession session = req.getSession(false); //obtain the salesman id from the session. String strSalesmanId = (String)session.getAttribute("SalesmanId"); /* Use the salesman id to search for clients that the salesman is supposed to visit. */ Collection clients = clientHome.findBySalesman(strSalesmanId); if(clients.size() <= 0) //No clients found { res.sendRedirect(res.encodeURL("/Status.jsp?code=4")); } /* Create an array of Vector objects. Each Vector object stores the Client id - first position in Vector Client Name - second position in Vector Client Address - third position in Vector It then stores this array of Vectors in the session */ Vector cinfo[] = new Vector[clients.size()]; Iterator i = clients.iterator(); int cnt=0; while(i.hasNext()) { Client client = (Client) i.next(); cinfo[cnt] = new Vector(3); cinfo[cnt].add(client.getId()); cinfo[cnt].add(client.getName()); cinfo[cnt].add(client.getAddress()); cnt++; } //Store the array of vectors in the session. Each Vector represents one client. session.setAttribute("ClientList",cinfo); //send redirection to ClientListJSP. res.sendRedirect(res.encodeURL("/ClientList.jsp")); }catch(Exception e){ System.out.println("Error while obtaining client list:" + e.getMessage()); res.sendRedirect(res.encodeURL("/Status.jsp?code=1")); } } private Context getInitialContext() throws NamingException { Context initial = null; try { /* There are other methods of obtaining the initial context too. Check the J2EE docs and your application server docs for these. */ initial = new InitialContext(); }catch(Exception ne) { System.out.println("Unable to get an initial context"); } return initial; } } </code></pre></td></tr></tbody></table> <p></p> <p>清單 5,<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#ClientList">ClientList.jsp</a> 獲得由 ClientViewServlet 放置在 Session 中的客戶清單;它顯示了客戶的姓名,但不是完整的詳細資訊(請參見圖 6)。當選擇一個使用者時,銷售人員則被導向到 ClientDetails.jsp。 </p> <p> </p> <h3><a name="ClientList">ClientList.jsp</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> <%@page contentType="text/vnd.wap.wml" session="true" import="java.util.Vector"%> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> <% //Obtain the client list, represented by an array of Vector objects, from the session. Vector cinfo[]; cinfo = (Vector[])session.getAttribute("ClientList"); %> <wml> <card id="ClientList" title="Client List"> <p align="center"> <% //Display Client names from cinfo[] if((cinfo.length <= 0) || (cinfo == null)) %> You Have No Clients To Visit Today <% else { /* The client list consists of a list of client names. Each name is a link to the ClientDetailsJSP. The URL for the link is constructed by appending the index within the client list array of vector objects. This is the index to the Vector object that this name is represented by. The ClientDetailsJSP uses this index to obtain the specific Vector object from the client list array and display the details of the specific client. Remember that the client list is stored in the session. */ for(int i=0;i<cinfo.length;i++) { %> <br/> <a href="/ClientDetails.jsp?ind=<%=i%>" title="<%=cinfo[i].get(1)%>"><%=cinfo[i].get(1)%></a> <% } } %> </p> <do type="prev" label="Back"> <prev/> </do> </card> </code></pre></td></tr></tbody></table> <p></p> <p><img height="334" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig6.gif" width="310"><br><br>注意︰顯示客戶清單的工作由三個獨立的部分完成 ─ ClientViewServlet、ClientList.jsp 和 ClientDetails.jsp。這樣設計的原因是什麼呢? <br><br>1.大多數 J2EE 架構的權威人士建議 JSP 不應該直接存取 EJB;而應使用諸如 Servlet 的中間件來進行與 EJB 的交互。ClientViewServlet 存取 EJB 並獲得客戶清單。 <br>2.這個應用本可以如此設計,這樣所有的使用者訊息都會包括在一個單一的 WML 文件中。在這個體系結構中,客戶清單包括在 WML 盒中的一張卡片上,而單個客戶的詳細資訊會包括在同一盒中的不同卡片上。不過該單一文件可能包括太多資料,以至於對一個低頻寬的 WAP 裝置來說不能立刻下載。如果客戶數目過於龐大,所生成的資料總量很容易超過 WML 卡片盒所容許的最大容量。(最大容量隨裝置不同而有所區別;如 Nokia 7110 的最大編譯卡片盒容量為 1.3 KB)。因此我們使用兩個 JSP︰ClientList.jsp ─ 顯示客戶清單,還有 ClientDetails.jsp ─ 顯示單個客戶的詳細資訊。 <br><br>清單 6,<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#ClientDetails">ClientDetails.jsp</a> 接受客戶陣列的索引號作為參數,其中索引號在 Session 中出現。接下來它獲得所選客戶的詳細資訊並顯示。如果銷售人員希望為該客戶下訂單,他祇要敲下 Items 按鈕。這會呼叫清單 7,<a href="http://www2.tw.ibm.com/developerWorks/tutorial/SelectTutorial.do?tutorialId=119#ItemListServlet">ItemListServlet</a>,並且顯示該訂單的可選產品。 </p> <p> </p> <h3><a name="ClientDetails">ClientDetails.jsp</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> <%@ page contentType="text/vnd.wap.wml" %> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> <% /* Obtain the client list from the session. Get the index of the Vector object, representing the specific client, from the URL. For a description of the structure of each Vector object see comments within ClientViewServlet. */ Vector cinfo[] = (Vector[])session.getAttribute("ClientList"); int index = 0; try { index = (int)Integer.parseInt(request.getParameter("ind")); }catch(NumberFormatException nfe) {} %> <wml> <card id="ClietDetails" title="<%=cinfo[index].get(1)%>"> <p align="center"> Id: <%=cinfo[index].get(0)%> <br/>Address: <%=cinfo[index].get(2)%> </p> <do type="prev" label="Back"> <prev/> </do> <do type="accept" label="Items"> <go href="/ItemListServlet?client=<%=cinfo[index].get(0)%>"/> </do> </card> </wml> </code></pre></td></tr></tbody></table> <p></p> <p> </p> <h3><a name="ItemListServlet">ItemListServlet</a></h3> <table cellspacing="0" cellpadding="3" width="100%" bgcolor="#cccccc" border="1"><tbody><tr><td><pre><code> import java.io.*; import javax.servlet.http.*; import javax.servlet.*; import javax.rmi.*; import javax.naming.*; import java.util.*; public class ItemListServlet extends HttpServlet { ItemHome itemHome; public void init() throws ServletException { /* This Servlet functions in a similar manner to the ClientViewServlet. Only in this case it obtains the item list from the Item Entity EJB. Each item is represented by the Item Entity EJB. */ try { System.out.println("trying to get initial context"); Context ic = (InitialContext) getInitialContext(); System.out.println("Got InitContext"); Object objRef = ic.lookup("Item"); System.out.println("Got obj ref"); itemHome = (ItemHome) PortableRemoteObject.narrow(objRef,ItemHome.class); System.out.println("Got Home"); }catch(Exception e) { System.out.println("Error in init"); e.printStackTrace(); } } public void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException { try { HttpSession session = req.getSession(false); String clientCode = (String)req.getParameter("client"); /* Store the client code in the session. This code is sent by the ClientDetailsJSP along with the request. */ session.setAttribute("ClientId",clientCode); Collection items = itemHome.findAll(); //find all items. if(items.size() <= 0) //No items for sale { System.out.println("items size: " + items.size()); res.sendRedirect(res.encodeURL("/Status.jsp?code=3")); } Vector itemInfo[] = new Vector[items.size()]; Iterator i = items.iterator(); int cnt=0; /* Structure of each Vector object Item Id - first position in Vector Item Name - second position in Vector Item Description - third position in Vector */ while(i.hasNext()) { Item item = (Item) i.next(); itemInfo[cnt] = new Vector(3); itemInfo[cnt].add(item.getId()); itemInfo[cnt].add(item.getName()); itemInfo[cnt].add(item.getDescription()); cnt++; } session.setAttribute("ItemList",itemInfo); //place the item list in the session. res.sendRedirect(res.encodeURL("/ItemList.jsp")); }catch(Exception e){ System.out.println("Error while obtaining item list:" + e.getMessage()); res.send/www.wapforum.org/DTD/wml_1.1.xml"> <% /* The place order obtains the Item code from the index passed to it. It then places this in the session. */ int index = Integer.parseInt(request.getParameter("ind")); Vector itemInfo[] = (Vector[])session.getAttribute("ItemList"); session.setAttribute("ItemId",itemInfo[index].get(1)); %> <wml> <card id="quantity" title="Quantity"> <p align="center"> <br/> Quantity: <input name="Quantity" type="text" emptyok="false" value="" size="20"/> </p> <do type="prev" label="Next"> <go href="/PlaceOrderServlet" method="get"> <postfield name="quant" value="Quantity"/> </go> </do> </card> </wml> PlaceOrder.jsp accepts from the user via an input field the number of items that the salesperson wants to order. It then places an order by calling PlaceOrderServlet. PlaceOrderServlet.java import java.io.*; import javax.servlet.http.*; import javax.servlet.*; import javax.rmi.*; import javax.naming.*; import java.util.*; import java.sql.*; import javax.sql.*; public class PlaceOrderServlet extends HttpServlet { Context ic; OrderHome home; javax.sql.DataSource ds; /* The PlaceOrder servlet first contacts the data source directly and obtains the highest primary key value(i.e. the order id). It then increments this value to obtain the next primary key value. It then creates a new Order Entity EJB, which corresponds to the new order placed. The order consists of the order id, salesman id, client id, item id and the quantity of items ordered. */ public void init() throws ServletException { try { System.out.println("trying to get initial context"); ic = (InitialContext) getInitialContext(); System.out.println("Got InitContext"); /* Lookup the datasource of this application. The name 'wapDB' is recognized by the application server from the configuration files. */ ds = (javax.sql.DataSource)ic.lookup("wapDB"); System.out.println("lookup for ds succeeded"); Object objRef = ic.lookup("Order"); home = (OrderHome)PortableRemoteObject.narrow(objRef,OrderHome.class); System.out.println("Got Home"); }catch(Exception e){ System.out.println("Error in init of PlaceOrderServlet: " + e.getMessage()); e.printStackTrace(); } } public void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException { try { HttpSession session = req.getSession(false); String salesman_id = (String)session.getAttribute("SalesmanId"); String client_id = (String)session.getAttribute("ClientId"); String quantity = (String)req.getParameter("quant"); String item_id = (String)session.getAttribute("ItemId"); String maxOrderId = null; Connection conn = ds.getConnection(); //Get the max order id PreparedStatement s = conn.prepareStatement("SELECT MAX(ID) FROM ORDER_INFO"); s.executeQuery(); ResultSet rs = s.getResultSet(); while(rs.next()) { maxOrderId = rs.getString(1); } maxOrderId = maxOrderId.trim(); long orderId = Long.parseLong(maxOrderId); //increment the old max id to obtain the new one. orderId++; String newOrderId = "" + orderId; //create a new Order EJB corresponding to the order just placed. Order order = home.create(newOrderId,quantity,item_id,salesman_id,client_id); String time = order.getTime(); session.setAttribute("OrderId",order.getPrimaryKey()); session.setAttribute("OrderTime",time); res.sendRedirect(res.encodeURL("/Confirm.jsp")); }catch(Exception e) { System.out.println("Error in doGet of PlaceOrderServlet: " + e.getMessage()); e.printStackTrace(); } } private Context getInitialContext() throws NamingException { Context initial = null; try { /* There are other methods of obtaining the initial context too. Check the J2EE docs and your application server docs for these. */ initial = new InitialContext(); }catch(Exception ne) { System.out.println("Unable to get an initial context"); } return initial; } } </code></pre></td></tr></tbody></table> <br><br><p></p> <p><img height="334" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig8.gif" width="310"><br><br>PlaceOrderServlet 從 Session 中獲得銷售人員、客戶及產品的 ID。接著透過建立新的 Order Entity EJB 可以產生一個新的訂單。成功的下單顯示了訂單的 ID 和下訂單的時間。 <br><br>在這一版本的應用中,銷售人員在完成交易後的唯一選擇便是返回主選單(請參見圖 9)。您也可修改程式碼以便使用者返回到客戶或產品清單。 <br><br><img height="334" src="http://www2.tw.ibm.com/developerWorks/images/tutorial/wireless/j2me/wapapplication/wapj2ee-fig9.gif" width="310"><br><br>如果注意觀察,您會發覺銷售人員只輸入兩次資料︰登入時和為客戶輸入購買產品數量時。 </p> <p> </p> <h3><a name="sec4">關於程式碼</a></h3>附帶文件包括本文所有的 JSP 和 Servlet 程式碼,也包括必需的 EJB 程式碼。EJB 的 jar 文件和部署描述項也一起包括在內。所有螢幕截圖均來自 Nokia WAP 模擬器 2.0 版。 <p></p> <p> </p> <h3><a name="sec5">結論</a></h3>就像前面提到的那樣,WAP 應用提供了非常好的增值服務。一個孤立的 WAP 應用是不可取的。然而,這樣一個應用無需花費很多財力人力就可以方便地整合到一個現有的 Web 應用體系結構中去。您所需要的唯一新硬體是一台機器,以及用於 WAP 閘道的軟體;如果您使用的是自己 ISP 的閘道,那麼這項開銷也可省去了。 <br><br>現有的 HTML 頁面需要被轉換成 WML。然而,WML 不像 HTML 那麼複雜,因為它不支援 HTML 的許多功能。因此,這並不是一件費時的工作。 <br><br>WAP 也支援無線 BitMap(WBMP)格式的圖片。然而,使用 WAP 裝置的使用者在連線時間上花費了不少錢,他們更感興趣的是直接有效的訊息而非奢華的界面。除非圖片本身能傳遞訊息,否則提供快速的訊息比占用頻寬和時間來傳輸圖片會更好。 <br><br>最後注意事項︰儘管模擬器可以提供測試 WAP 應用的良好環境,但祇有當它配合已部署好的 WAP 閘道,執行在所有可能的目標 WAP 裝置上時,WAP 應用才算作真正意義上的被完全測試過了。所有動態生成的頁面在閘道上被編譯。因此,有必要知道您的閘道支援哪些版本的 WAP。如果閘道編譯器只使用 WML 1.1,那麼用 WML 1.2 編寫的頁面是毫無用處的。 <p></p> <p> </p> <h3><a name="sec6">參考資料</a></h3> <ul type="disc"> <li>從 <a href="http://developer.java.sun.com/developer/products/j2ee/">Sun 的 Java 工作人員連線</a>那裡獲得 J2EE 的介紹(需免費註冊)。 </li> <li> <a href="http://www-106.ibm.com/developerWorks/wireless/library/wi-wap/">Bilal Siddiqui 撰寫的「WAP 網站創作」(developerWorks,2001 年 5 月)</a>非常好地介紹了 WML 和 WMLScript。 </li> <li>從 <a href="http://www.wapforum.org/">WAP 論壇</a>中獲得更多關於 WAP、WML 和 WMLScript 的訊息,該論壇是一個致力於維護 WAP 標準的開放的獨立組織。 </li> <li>IBM 的 <a href="http://www-4.ibm.com/software/webservers/appserv/&origin=wi">Websphere Application Server</a> 可用來執行本文提到的應用。 </li> <li>您可以從 Nokia 下載 WAP 閘道軟體和 WAP 裝置模擬器的<a href="http://www.nokia.com/nokia/0,1522,,00.html?orig=/wap/index.html">展示版</a>。 </li> <li> <a href="http://www-3.ibm.com/software/awdtools/vapacbase/vapwap.htm">Salmon 和 IBM</a>為您的無線網路帶來企業 Java 技術。 </li> <li>返回 developerWorks 的 <a href="http://www2.tw.ibm.com/developerWorks/wireless">無線</a>專區或 <a href="http://www2.tw.ibm.com/developerWorks/java">Java 技術</a>專區以獲取更多關於無線和 Java 技術的參考資料。 </li> </ul> <p></p> <p> </p> <h3><a name="sec7">關於作者</a></h3>Aashish Patil 不久前剛獲得印度 Mumbai 的 Thadomal Shahani 工程技術學院的計算機工程系學士學位。在 Tata Consultancy Services,他作為實習生完成了一個關於支援 WAP 的股票交易的專案;本文是該專案的一個直接部分。今年秋季,他將赴 Southern California 大學攻讀計算機科學系的碩士學位。可透過<a href="mailto:ash01@vsnl.net"> ash01@vsnl.net </a>連絡 Aashish。 </tutorialcontent>