Электронный магазин на Java и XML

         

Безопасность


Безопасность — очень важный фактор для любого коммерческого web-сайта. Чтобы гарантировать безопасность, нужно придерживаться определенных принципов. Следует определить все риски и разработать план снижения этих рисков. Постоянный учет разработанных принципов позволяет обеспечивать безопасность даже в быстроменяющихся условиях. Следует обратить внимание, в частности, на следующие факторы:

конфиденциальность информации (information confidentially) — защита передаваемой или хранящейся на сайте информации;

аутентификация (authentication) — проверка регистрационной информации пользователя;

авторизация (authorization) — ограничение прав клиента при взаимодействии с системой;

целостность данных (data integrity) — гарантия того, что принятые по сети данные не изменены при пересылке случайно или преднамеренно;

отсутствие сбоев (non-repudiation) — обеспечение защищенности источника данных и целостности данных,

доступность (availability) — гарантия того, что система доступна всегда, когда это требуется.

Подробное изучение всех этих факторов выходит за рамки нашей книги, но существуют некоторые специфические вопросы, связанные с обработкой кредитных карт, которые мы рассмотрим в этой главе. При работе с персональной информацией клиента и особенно с номером его кредитной карты безопасность является предельно важным фактором.

Первой линией защиты является шифрование важной информации, которая пересылается между браузером пользователя и сервером электронного магазина. Обычно для этого используется протокол HTTPS (Secure Hypertext Transfer Protocol — безопасный протокол передачи гипертекста), в котором все данные, пересылаемые между браузером и сервером, шифруются с помощью технологии SSL (Secure Sockets Layer — слой защищенных сокетов). Если данные не зашифрованы, злоумышленники могут с легкостью прочесть информацию, которая пересылается между браузером и сервером.

Далее, все источники данных, в которых хранится важная информация, также должны быть зашифрованы. Если номера кредитных карт хранятся в базе данных, то эта база данных должна быть зашифрована во избежание несанкционированного доступа. Если данные хранятся в файле XML, то номера кредитных карт или сам файл должен быть зашифрован. Кроме того, данные должны храниться на компьютере, к которому невозможен доступ через Интернет.


На всех компьютерах следует использовать брандмауэры. Правильно сконфигурированные средства защиты могут значительно снизить риск несанкционированного доступа к информации. Отсутствие брандмауэра позволяет злоумышленникам осуществить все виды нападений на ваш сайт, так что эти средства защиты необходимы для любого коммерческого web-сайта.

Тщательно протестируйте вашу систему. Ошибки в системе часто оказываются теми лазейками, из-за которых не срабатывают средства защиты. Этого не произойдет, если система работает без сбоев. Поэтому тестирование системы имеет очень важное значение. Установка средств защиты зависит от программного обеспечения, используемого при реализации системы. Для установки протокола HTTPS необходимо, чтобы web-сервер был правильно сконфигурирован. Для шифрования базы данных также необходимо, чтобы она была правильно сконфигурирована. Мы не будем углубляться в детали установки средств обеспечения безопасности, так как они весьма разнообразны, но важность этого фактора трудно переоценить.






Доверие клиента


Если клиент не испытывает доверия к web-сайту, у него не возникнет желания передавать информацию, связанную с его кредитной картой. Между чтением информации на страницах электронного магазина и решением фактически приобрести товар — огромная разница. Можно посоветовать несколько способов повышения доверия клиента к web-сайту.

Качество работы сайта влияет на впечатления клиента от вашего электронного магазина. Если клиент сталкивается с неработающими ссылками или другими очевидными системными ошибками, вряд ли он будет относиться к такому сайту с доверием. Когда речь идет только о поиске информации, то подобные ошибки могут восприниматься достаточно легко, но когда дело доходит до сообщения важной персональной информации, надежная работа сайта становится критически важной.

Доступное изложение принципов работы с персональной информацией также очень важно для формирования доверительного отношения клиента. Если клиент не имеет представления о том, что произойдет с той информацией, которую он должен переслать на ваш сайт, он вряд ли захочет это сделать. Поэтому на сайте следует отвести специальную страницу, где четко объясняется, зачем нужна эта информация и как она будет обрабатываться; кроме того, необходимо, чтобы имелась четко обозначенная ссылка на эту страницу.



Неплохой идеей является предоставление клиенту возможности ознакомиться с теми мерами, которые предприняты для обеспечения безопасности при пересылке информации. Если вы объясните, что вся персональная информация зашифровывается перед отправлением и хранится в безопасном месте, клиент будет больше доверять вашему сайту.

Также на отношение клиента влияет общее впечатление от вашего сайта. Чем лучше выглядит ваш сайт, тем больше доверия он вызовет у посетителя. Оформление сайта, удобство навигации и другие факторы, влияющие на впечатление клиента, также будут влиять и на степень его доверия к сайту. Грамматические ошибки в тексте, неряшливое оформление и неудобство навигации неизбежно оттолкнут потенциальных покупателей.

 



JSP-страница Approved


Эта страница вызывается из сервлета SubmitOrder, когда заказ получает подтверждение поставщика услуг по обработке. Эта простая страница включает в себя идентификатор заказа (листинг 5.26); ее можно использовать как квитанцию и при необходимости распечатать данные заказа. Так как у нас имеется объект Order с полной информацией о заказе, в страницу Approved можно включить помимо идентификатора и другие характеристики заказа.

Листинг 5.26. JSP-страница Approved (Approved.jsp)

<%@ page import="com.XmlEcomBook.Chap05.*" %>

<html>

<head><title>Approved Order</title></head>

<body>

<jsp:useBean id="order" scope="session" class="Order" />

Your order has been approved.

Your order number is: <jsp:getProperty name="order" property="id" />

</body>

</html>



JSP-страница Confirmlnfo


Первое, что мы делаем в JSP-странице Confirmlnfo, — это помещаем информацию, введенную в форму на JSP-странице Creditlnfo, в объект Creditlnfo. При этом используются элементы jsp:useBean и jsp:setProperty, как и в двух предыдущих JSP-страницах. После того как получен объект Order, скриптлет извлекает из него объект Customer-Information, а затем вызывается метод setCreditlnfo, чтобы записать полученные значения, как показано в листинге 5.22.

Листинг 5.22. Запись данных кредитной карты (Confirmlnfo.jsp)

<%@ page import="com.XmlEcomBook.Chap05.*" %>

<jsp:useBean scope="session" id="creditInfo" class="CreditInfo" />

<jsp:setProperty name="creditInfo" property="*" />

<jsp:useBean scope="session" id="order" class="Order" />

<% CustomerInfo cust = order.getCustomerInfo();

cust.setCreditInfo( creditInfo );

%>

Код HTML этой страницы вновь отображает все введенные клиентом данные. Это позволяет клиенту проверить всю введенную им информацию до того, как она будет окончательно отправлена на сервер поставщика для получения подтверждения. Для этого берутся объекты Customerlnfo и Fullfilment, связанные с данным сеансом, и с помощью элементов jsp:getProperty из них извлекаются различные параметры заказа, которые затем отображаются на странице. Внизу страницы располагается кнопка, которая позволяет клиенту отправить данные на сервер, если он не обнаружил ошибок. Чтобы исправить неверно введенные данные, клиент может воспользоваться кнопкой Back (Назад) в окне браузера. Листинг 5.23 содержит код для отображения и проверки введенных клиентом данных.

Листинг 5.23. Отображение информации для ее подтверждения клиентом (Confirmlnfo.jsp)

<html>

<head><title>Confirm Info</title></head>

<body>

<jsp:useBean scope="session" id="custInfo" class="CustomerInfo" />

<jsp:useBean scope="session" id="shippingInfo" class="Fulfillment" />


<p>Verify the information you entered:</p>

<p>Name: <b><jsp:getProperty name="custInfo" property="firstName" />

<jsp:getProperty name="custInfo" property="lastName" /></b>

</p>

<p>

Address:<br />

<b><jsp:getProperty name="custInfo" property="address1" /> <br />

<jsp:getProperty name="custInfo" property="address2" /><br />

<jsp:getProperty name="custInfo" property="city" />,

<jsp:getProperty name="custInfo" property="state" />

<jsp:getProperty name="custInfo" property="zip" />

</b></p>

<p>Email: <b><jsp:getProperty name="custInfo" property="email" /></b></p>

<p>Phone Number: <b><jsp:getProperty name="custInfo" property="phoneNumber" />

</b></p>

<p>

Credit Card Type : <b><jsp:getProperty name="creditInfo"

property="creditCardType" /></b><br />

Credit Card Number: <b><jsp:getProperty name="creditInfo"

property="creditCardNumber" /></b><br />

Expiration Date : <b><jsp:getProperty name="creditInfo"

property="expirationDate" /></b>

</p>

<p>

Shipper: <b><jsp:getProperty name="shippingInfo"

property="shipper" /></b><br />

Class : <b><jsp:getProperty name="shippingInfo"

property="shippingClass" /></b>

</p>

<b><i> Press the back button on your browser to correct any information.</i></b>

<form action="servlet/SubmitOrder">

<input type="submit" value="Submit Order" />

</form>

</body>

</html>






JSP-страница Creditlnfo


Объект Shippinglnfo, связанный с определенным сеансом, создается в JSP-странице Creditlnfo. Информация о доставке, которая была введена в форму в JSP- странице Shippinglnfo, используется в JSP-странице Creditlnfo. Выбранный способ доставки заносится в параметр с именем shipperAndClass, а затем элемент jsp:setProperty вызывает метод setShipperAndClass объекта Shippinglnfo со значением этого параметра. Как уже говорилось ранее, при этом задается сразу и почтовая фирма, и вид доставки, то есть поля shipper и shippingClass (листинг 5.19).

Листинг 5.19. Указание способа доставки (Creditlnfo.jsp)

<%@ page import="com.XmlEcomBook.Chap05.*" %>

<jsp:useBean scope="session" id="shippingInfo" class="Fulfillment" />

<jsp:setProperty name="shippingInfo" property="*" />

В этой JSP-странице сначала задается объект Fullfilment, связанный с данным сеансом и содержащийся в объекте Order. Также создается объект ShippingCalculator, который используется для подсчета выбранного способа доставки. После того как знак $ убран из строки с указанием цены, Shippinglnfo преобразуется к типу double и используется для вызова метода setCostToCustomer объекта Shippinglnfo (листинг 5.20).

Листинг 5.20. Определение стоимости доставки (Creditlnfo.jsp)

<jsp:useBean scope="session" id="order" class="Order" />

<% order.setFulfillment( shippingInfo );

ShippingCalculator calc = new ShippingCalculator( order );

String s = request.getParameter( "shipperAndClass" );

String price = calc.getPrice( s );

price = price.replace( '$', ' ' );

shippingInfo.setCostToCustomer ( Double.parseDouble( price ) );%>

Код HTML этой JSP-страницы используется для сбора необходимой информации о кредитной карте клиента. В первую очередь мы хотели бы отобразить в таблице полностью всю сумму, которую мы собираемся снять со счета клиента. Мы показываем общую стоимость покупки, стоимость доставки и суммарную стоимость заказа. Далее выводится форма, в которой клиент указывает тип своей кредитной карты, номер и дату окончания срока действия, как показано в листинге 5.21.




Листинг 5.21. Код HTML, который формируется в JSP-странице Creditlnfo (Creditlnfo.jsp)

<html>

<head><title>Credit Card Information</title></head>

<body>

Your order price<br />

<table>

<tr><td>Items</td><td><%=order.getTotalItemPrice()%> </td></tr>

<tr><td>Shipping</td><td><%=price%></td></tr>

<tr bgcolor="yellow"><td>Total</td><td> <%=order.getOrderTotal()%></td></tr>

</table>

Please enter your credit card information:

<form action="ConfirmInfo.jsp">

<p>

Credit Card Type:

<input type="radio" name="creditCardType" value="Visa">

Visa

</input>

<input type="radio" name="creditCardType" value="Master Card">

Master Card

</input>

<input type="radio" name="creditCardType" value="American Express">

American Express

</input>

<input type="radio" name="creditCardType" value="Discover">

Discover

</input>

</p>

<p>Credit Card Number:<input name="creditCardNumber" /> </p>

<p>Expiration Date:<input name="expirationDate" /></p>

<input type="submit" value="Submit information">

</form>

</body>

</html>

 




JSP-страница Declined


JSP-страница Declined формируется, если по какой-либо причине кредитная карта клиента не прошла проверку поставщика. Страница генерирует сообщение, в котором указывается эта причина.

Листинг 5.27. JSP-страница Declined (Declined.jsp)

<%@ page import="com.XmlEcomBook.Chap05.*" %>

<html>

<head><title>Credit Card Declined</title></head>

<body>

<jsp:useBean id="order" scope="session" class="Order" />

Your credit card was declined.<br />

The reason given was:

<% Authorization auth = order.getAuthorization();

if( auth != null ) {

out.println( auth.getReason() );

}

%>

</body>

</html>

 



JSP-страница OrderDateSelector


Простая JSP-страница OrderDateSelector (листинг 5.28) отображает форму с единственным полем ввода. В этом поле по умолчанию указана текущая дата, но пользователь (в данном случае работник отдела доставки) может ввести другую дату. Эта JSP-страница является точкой входа в процесс обновления информации о доставке.

Листинг 5.28. JSP-страница OrderDateSelector (OrderDateSelector.jsp)

<%@ page import="java.util.*" %>

<html>

<head><title>0rder Date Selector</title></head>

<body>

<% Calendar calendar = new GregorianCalendar();

int day = calendar.get( Calendar.DAY_OF_HONTH );

int month = calendar.get( Calendar.MONTH ) + 1;

int year = calendar.get( Calendar.YEAR ); %> <form action="SelectOrder.jsp"> Select a date: <input name="date" value="

<%= new String( year + "-" + month + "-" + day ); %>">

<br /xinput type="submit" value="Get Orders for Date">

</form>

</input>

</body>

</html>

 



JSP-страница SelectOrder


JSP-страница SelectOredr (листинг 5.29) использует заранее определенную (стандартную) переменную request, чтобы получить введенную пользователем в JSP- страницу OredrDateSel ector дату. Эта дата требуется впоследствии для создания имени папки. Так как имя файла XML, содержащего сведения о заказе, включает в себя идентификатор данного заказа, все, что нужно сделать для отображения заказов, — это выделить идентификатор заказа из имени файла. Используя этот идентификатор, мы для каждого заказа создаем элемент HTML а для ссылки на JSP-страницу. Каждая ссылка содержит имя выбранного файла XML и папки, в которой этот файл содержится. Пользователь может просто щелкнуть на этой ссылке и увидеть соответствующий заказ. Например, если заказ с идентификатором 1014 был сделан 16 октября 2000 года, то ссылка будет иметь вид:

<а href="ShowOrder.jsp?dir=

Orders_2000-10-16&file=0rder_1014.xml">1014</a>

Листинг 5.29. JSP-страница SelectOrder (SelectOrder.jsp)

<%@ page import="java.io.*" %>

<html>

<head><title>Select Order</title></head>

<body>

<%

String date = request.getParameter( "date" );

File dir = new File( "Orders_" + date );

File[] files = dir.listFiles();

for( int i = 0; i < files.length; i++ ) {

String name = files[i].getName();

if( name.endsWith( ".xml" ) ) {

int start = name.indexOf( '_' ) + 1;

int end = name.indexOf( '.' );

String orderNum = name.substring( start, end );

%>

<a href="ShowOrder.jsp?dir=<%= dir %>&file=<%= name %> ">

<%= orderNum %>

</a><br />

<% }

}

%>

</body>

</html>



JSP-страница Shippinglnfo


После импорта необходимых классов для получения нового объекта класса Custo- merlnfo, связанного с текущим сеансом, используется элемент jsp:useBean. Затем свойства этого элемента с помощью элемента jsp:setProperty устанавливаются равными значениям, введенным в форму на странице Customerlnfo.html. После этого мы получаем класс Order, связанный с текущим сеансом, и вектор Vector, в котором содержится перечень всех заказанных товаров. Потом с помощью скриптлета [Скриптлетом (scriptlet) авторы называют код JSP внутри тегов <%...%>, по-видимому, по аналогии с апплетом и сервлетом (еще одним «изобретением» автора). — Примеч. ред. ] JSP вызываются методы setCustomerlnfo и setltems, которые добавляют соответствующие объекты в объект Order, как показано в листинге 5.17.

Листинг 5.17. Начало кода Shippinglnfo JSP (Shippinglnfo.jsp)

<%@ page import="com.XmlEcomBook.Chap05.*,java.util.*" %>

<jsp:useBean scope="session" id="custInfo" class="CustomerInfo" />

<jsp:setProperty name="custInfo" property="*" />

<jsp:useBean id="order" scope="session" class="Order" />

<jsp:useBean id="theorder" scope="session" class="Vector" />

<% order.setCustomerInfo( custInfo );

order.setItems( theorder );

%>

Затем JSP-страница выдает код HTML, который позволяет пользователю выбрать один из предложенных способов доставки. В этом коде используется объект ShippingCalculator, рассмотренный нами выше в этой главе. Скриптлет, встроенный в код HTML, создает объект ShippingCalculator, а затем с помощью метода getTypes предлагает несколько способов доставки. На этом этапе создается ряд переключателей (radio buttons), по одному на каждый из указанных способов. Это делается с помощью цикла for. Для каждого способа создается элемент input типа radio. Атрибут value (значение) элемента устанавливается с помощью выражения JSP "<t=types[i]X>". Затем такое же выражение используется для отображения этого значения на странице. Другой скриптлет обеспечивает получение данных о стоимости того или иного способа доставки с помощью метода getPrice объекта ShippingCalculator, как показано в листинге 5.18.




Листинг 5.18. Код HTML, который формируется в JSP-странице Shippinglnfo (Shippinglnfo.jsp)

<html>

<head>

<title>Shipping Info</title>

</head>

<body>

<form action="CreditInfo.jsp">

Select a Shipper and Class:<br />

<table>

<% ShippingCalculator calc = new ShippingCalculator( order );

String[] types = calc.getTypes();

for( int i = 0; i < types.length; i++ ) {

%> <tr><td><input type="radio" name="shipperAndClass" value="<%=types[i] %>" /><%=types[i] %></td>

<td><%=calc.getPrice(types[i]) %></td></tr>

<% } %>

</table>

<input type="submit" value="Submit information">

</form>

</body>

</html>






JSP-страница ShowOrder


Эта страница отображает данные о заказе, который был выбран пользователем для просмотра. Для создания объектной модели документа задействуются выбранные с помощью описанной выше страницы файл и папка. Используя скрипт- леты (код внутри тегов <%..%>), мы производим анализ файла с помощью стандартных методов DOM. Результат этого разбора записывается в JSP-выражения (код внутри тегов <*=!...*>). Страница ShowOrder представлена в листинге 5.30. Хотя ее код довольно длинный, многие кодовые фрагменты повторяются.

Листинг 5.30. JSP-страница ShowOrder (ShowOrder.jsp)

<%@ page

import="javax.xml.parsers.*,java.util.*,java.io.*,org.w3c.dom.*, org.xml.sax.*"

%>

<html>

<head><title>Order</title></head>

<body>

<%

double price = 0.0;;

String dir = request.getParameter( "dir" );

String file = request.getParameter( "file" );

Document document = null;

DocumentBuilderFactory factory

= DocumentBuilderFactory.newInstance();

try {

DocumentBuilder builder = factory.newDocumentBuilder();

document = builder.parse( new File( dir, file ) );

}

catch( ParserConfigurationException pce ) {

throw new IOException( "Parser Configuration Error" );

}

catch( SAXException se ) {

throw new IOException( "Parsing Excpetion" );

}

Element order = document.getDocumentElement();

String id = order.getAttribute( "id" );

%>

<h1>Order #<%=id%></h1>

<h2>Items</h2>

<table border="1">

<tr><th>Item</th><th>Description</th><th>Quantity</th> <th>Price</th></tr>

<% NodeList items = order.getElementsByTagName( "item" );

int numItems = items.getLength();

for( int i = 0;i < numItems; i++ ) {

Element item = (Element)items.item( i );

%>

<tr><td><%=item.getAttribute( "id" )%></td>

<td><%=item.getFirstChild().getNodeValue()%></td>


<td><%=item.getAttribute( "quantity" )%></td>

<td><%=item.getAttribute( "price" )%></td>

<% String priceString = item.getAttribute( "price" );

priceString = priceString.replace( '$', ' ' );

price += Double.parseDouble( priceString ); %>

</tr>

<% }%>

</table>

<% NodeList n1 = order.getElementsByTagName ( "customer_info" );

Element cust = (Element)n1.item( 0 );

Node firstName = cust.getElementsByTagName ( "first_name").item(0);

Node lastName = cust.getElementsByTagName ( "last_name" ).item(0);

Node address1 = cust.getElementsByTagName ( "address1" ).item(0);

Node address2 = cust.getElementsByTagName ( "address2" ).item(0);

Node city = cust.getElementsByTagName( "city" ).item(0);

Node state = cust.getElementsByTagName( "state" ).item(0);

Node zip = cust.getElementsByTagName( "zip" ).item(0);

Node email = cust.getElementsByTagName( "email" ).item(0);

Node phone = cust.getElementsByTagName( "phone" ).item(0);

%>

<h2>Customer Information</h2>

Name:

<%=firstName.getFirstChild().getNodeValue()%>

<%=lastName.getFirstChild().getNodeValue()%><br /><br />

Address:<br />

<%=address1.getFirstChild().getNodeValue()%><br/ >

<%=address2.getFirstChild().getNodeValue()%><br />

<%=city.getFirstChild().getNodeValue()%>

<%=state.getFirstChild().getNodeValue()%>

<%=zip.getFirstChild().getNodeValue()%><br /><br />

Email:<%=email.getFirstChild().getNodeValue()%><br />

Phone:<%=phone.getFirstChild().getNodeValue()%><br />

<% NodeList n2 = order.getElementsByTagName( "credit_info" );

Element credit = (Element)n2.item( 0 );

Node number = credit.getElementsByTagName ( "card_number").item(0);

Node type = credit.getElementsByTagName ( "card_type").item(0);



Node exp = credit.getElementsByTagName ( "expiration_date").item(0);

%>

<h2>Credit Card Information</h2>

Type:<%=type.getFirstChild().getNodeValue()%><br />

Number:<%=number.getFirstChild().getNodeValue()%><br />

Expiration Date:<%=exp.getFirstChild().getNodeValue()%><br />

<% NodeList n3 = order.getElementsByTagName ( "authorization" );

Element auth = (Element)n3.item( 0 );

String approved = auth.getAttribute( "approved");

Node reason = auth.getElementsByTagName ( "reason").item(0);

Node auth_code = auth.getElementsByTagName ( "authorization_code").item(0);

%>

<h2>Authorization Information</h2>

Auth Code:<%=auth_code.getFirstChild().getNodeValue()%>

<br />

Approved:<%=approved%><br />

Reason:<%=reason.getFirstChild().getNodeValue()%><br />

<% NodeList n4 = order.getElementsByTagName( "fulfillment" );

Element fulfillment = (Element)n4.item(0);

Node shipper = fulfillment.getElementsByTagName ( "shipper" ).item(0);

Node clas = fulfillment.getElementsByTagName( "class" ).item(0);

Node cost = fulfillment.getElementsByTagName( "cost" ).item(0);

Node tracking = fulfillment.getElementsByTagName ( "tracking_number" ).item(0);

Node dateSent = fulfillment.getElementsByTagName ( "date_sent" ).item(0);

String trackingString = tracking.getFirstChild().getNodeValue();

String dateSentString = dateSent.getFirstChild().getNodeValue();

%>

В нижней части этой JSP- страницы располагается форма, предназначенная для ввода номера отслеживания заказа и даты его отправки. Эти значения можно вводить в том случае, если в поле для номера указано значение NO_TRA- CKING_NUMBER (номер отсутствует). Эта строка используется для указания, что данное поле еще не инициализировано, так что пользователь может ввести новое значение. Если же значения уже заданы, они просто отобразятся и пользователь уже не сможет их редактировать. В этой форме (листинг 5.31) также имеется некоторое количество скрытых полей, которые содержат информацию, необходимую для JSP-страницы, обновляющей файл XML.





Листинг 5.31. Форма для ввода данных о доставке (ShowOrder.jsp)

<form action="UpdateFulfillment.jsp">

<input type="hidden" name="dir" value="<%=dir%>" />

<input type="hidden" name="file" value="<%=file%>" />

<input type="hidden" name="email"

value="<%=email.getFirstChild().getNodeValue()%>" />

<input type="hidden" name="id" value="<%=id%>" />

<input type="hidden" name="auth_code"

value="<%=auth_code.getFirstChild().getNodeValue()%>" />

<input type="hidden" name="price" value="<%=price%>" />

<h2>Fulfillment Info</h2>

Shipper:<%=shipper.getFirstChild().getNodeValue()%><br />

Class:<%=clas.getFirstChild().getNodeValue()%><br />

Cost:$<%=cost.getFirstChild().getNodeValue()%><br />

Tracking #:

<%if( trackingString.equals( "NO_TRACKING_NUMBER" ) ) { %>

<input name="tracking" value="<%=trackingString%>" />

Date Sent:<input name="date_sent" value="<%=dateSentString%>" />

<input type="submit" value="Submit New Fulfillment Data" />

<%} else {%>

<%=trackingString%><br />

Date Sent:<%=dateSentString%>

<%}%>

</form>

<br /><a href="OrderDateSelector.jsp"> Back to date selection</a>

</body>

</html>

 




JSP-страница UpdateFullfilment


Последняя JSP-страница, которую мы исследуем в этой главе, отвечает за обновление значений номера отслеживания и даты отправления, введенных на странице ShowOrder. На этой JSP-странице мы применим другой способ обработки файла XML. Перед нами стоит довольно-таки простая задача: нужно обновить значения двух переменных в файле XML. Вместо того чтобы использовать DOM и проводить анализ документа, а затем снова записывать его в виде файла XML, мы применим более простой метод текстовой обработки. Элементам, которые нам нужны, — tracki ng_number и date_sent — при создании файла XML были присвоены специальные значения, NO_TRACKING_NUMBER и NO_DATE_SENT. Обновление файла XML в данном случае сводится к элементарному контекстному поиску этих строк и замене их на новые значения.

Сначала JSP-страница получает пересланные ей параметры, включая скрытые поля формы. Для считывания файла XML создается объект BufferReader, a объект FileWriter используется для вывода обновленного файла XML. Каждая прочитанная строка XML передается методу replace для выполнения необходимых замен. Этот метод replace аналогичен методу replace класса String, единственное отличие заключается в том, что он оперирует не символами (то есть объектами типа char), а строками.

Описанная техника обработки текста в простых случаях пригодна для обработки документов XML. Ее использование позволяет избежать дополнительных сложностей и расходов, связанных с анализом документа XML. Но эта техника не всегда хорошо работает, если требуется произвести какие-либо сложные замены. Работая с документом XML как с простым текстовым документом, вы создаете слишком чувствительный к внешним условиям код, который не будет работать при любых изменениях DTD или формата XML. Использование анализатора кода является наилучшим решением, если требуется достаточно серьезная обработка документа XML.

После того как в документ XML внесены необходимые изменения, эта JSP- страница посылает клиенту с помощью класса Emailег сообщение, подтверждающее выполнение его заказа. Также она информирует поставщика услуг по обработке, что оформление заказа закончено, поэтому можно снимать деньги со счета клиента и переводить их на счет магазина, как показано в листинге 5.32.




Листинг 5.32. JSP-страница UpdateFullfilment (UpdateFullfilment.jsp)

<%@ page import="java.io.*,com.XmlEcomBook.Chap05.*" %>

<html>

<head><title>Update Complete</title></head>

<body>

<% String tracking = request.getParameter( "tracking" );

String dateSent = request.getParameter( "date_sent" );

String dir = request.getParameter( "dir" );

String filename = request.getParameter( "file" );

String email = request.getParameter( "email" );

String id = request.getParameter( "id" );

String auth_code = request.getParameter( "auth_code" );

String priceString = request.getParameter( "price" );

double price = Double.parseDouble( priceString );

File inFile = new File( dir, filename );

File outFile = new File( dir, filename + ".tmp" );

BufferedReader reader = new BufferedReader ( new FileReader( inFile ) );

FileWriter writer = new FileWriter( outFile );

String line;

while( (line = reader.readLine()) != null ) {

String newLine = replace( line, "NOT_SENT_YET", dateSent );

newLine = replace( newLine, "NO_TRACKING_NUMBER", tracking );

writer.write( newLine + "\n" );

}

reader.close();

writer.close();

inFile.renameTo( new File( dir, filename + ".old" ) );

outFile.renameTo( inFile );

Emailer.sendShipped( email, id );

TestPaymentAuthorizer.capture( auth_code, price );

%>

<p> The fulfillment was updated with the new information.</p>

<a href="OrderDateSelector.jsp">Back to date selection</a>

</body>

</html>

<%!

String replace( String s, String oldString, String newString ) {

int pos = s.indexOf( oldString );

String newLine = s;

if( pos != -1 ) {

newLine = s.substring( 0, pos );

newLine += newString;

newLine += s.substring( pos + oldString.length() );

}

return newLine;

}

%>

Листинги программ, приведенные в этой главе, показывают, как можно реализовать необходимые для работы магазина функции по обработке заказов и составлению счетов. Чтобы использовать эти программы в реальном магазине, необходима некоторая доработка. Наибольшие изменения будут касаться требований безопасности. JSP-страницы, которые задействуются в процессе ввода информации клиентом, должны использовать не простой протокол HTTP, a HTTPS. Далее, в нашем примере информация о клиентах хранилась в незашифрованном файле в той же системе файлов, в которой работает web-сервер магазина. В идеале вся персональная информация о клиентах должна храниться в зашифрованном, безопасном источнике данных на изолированном сервере, к которому нет доступа из Интернета.

С другой стороны, необходимо заменить класс, отвечающий за проверку номеров кредитных карт, на класс, который будет взаимодействовать с реальным поставщиком услуг по обработке. Но в процессе тестирования системы, на том этапе, когда еще не нужно взаимодействие с реальным поставщиком, можно воспользоваться приведенным здесь классом.


Класс Authorization


Класс Authorization (листинг 5.4) используется для хранения данных, которые возвратил поставщик услуг по обработке. В нем имеются три поля: во-первых, булева переменная, указывающая, было ли получено подтверждение указанных клиентом сведений о кредитной карте. Затем имеется поле, в котором указываются причины отказа (если подтверждение не получено), и, наконец, поле, содержащее код подтверждения (если оно получено). Для всех этих полей имеются методы getXxx и setXxx, с помощью которых можно извлекать и модифицировать значения полей.

Листинг 5.4. Класс Authorization (Authorization.java)

package com.XmlEcomBook.Chap05;

public class Authorization {

private boolean approved = false;

private String reason = "Unknown";

//reason for a denial

private String authorizationCode;

// auth code from patment service

public boolean isApproved() {

return approved;

}

public void setApproved( boolean newApproved ) {

approved = newApproved;

}

public String getReason() {

return reason;

}

public void setReason( String newReason ) {

reason = newReason;

}

public String getAuthorizationCode() {

return authorizationCode;

}

public void setAuthorizationCode( String newAuthCode ) {

authorizationCode = newAuthCode;

}

}



Класс Creditlnfo


В классе Creditlnfo содержится информация о кредитной карте клиента. Сюда входит тип кредитной карты, например Visa или MasterCard, ее номер и дата истечения срока действия. Все эти сведения необходимо сообщить при обращении к поставщику услуг по обработке.

Листинг 5.2. Класс Crediditlnfo (Creditlnfo.java)

package com.XmlEcomBook.Chap05;

public class CreditInfo extends Object {

private String creditCardType;

private String creditCardNumber;

private String expirationDate;

public CreditInfo() {

}

public String getCreditCardType() {

return creditCardType;

}

public void setCreditCardType( String newCreditCardType ) {

creditCardType = newCreditCardType;

}

public String getCreditCardNumber() {

return creditCardNumber;

}

public void setCreditCardNumber( String newCreditCardNumber)

{

creditCardNumber = newCreditCardNumber;

}

public String getExpirationDate() {

return expirationDate;

}

public void setExpirationDate( String newExpirationDate ) {

expirationDate = newExpirationDate;

}

}



Класс Customerlnfo


Класс Customerlnfo предназначен для сбора некоторой стандартной информации о покупателе. Имя покупателя и его адрес необходимы для того, чтобы ему можно было доставить заказ, а адрес электронной почты и номер телефона — для того, чтобы можно было связаться с клиентом и сообщить ему о каких-то изменениях или задать какие-либо вопросы. Объект Creditlnfo мы обсудим несколько позже. Класс Customerlnfo состоит только из методов getXxx и setXxx, как показано в листинге 5.1.

Листинг 5.1. Класс Customerlnfo (Customerlnfo.java)

package com.XmlEcomBook.Chap05;

public class CustomerInfo {

private String lastName;

private String firstName;

private String address1;

private String address2;

private String city;

private String state;

private String zip;

private String email;

private String phoneNumber;

private CreditInfo creditInfo;

public CustomerInfo() {

}

public String getLastName() {

return lastName;

}

public void setLastName( String newLastName ) {

lastName = newLastName;

}

public String getFirstName() {

return firstName;

}

public void setFirstName( String newFirstName ) {

firstName = newFirstName;

}

public void setAddress1( String newAddress1 ) {

address1 = newAddress1;

}

public String getAddress1() {

return address1;

}

public void setAddress2( String newAddress2 ) {

address2 = newAddress2;

}

public String getAddress2() {

return address2;

}

public void setCity( String newCity ) {

city = newCity;

}

public String getCity() {

return city;

}

public void setState( String newState ) {

state = newState;

}

public String getState() {

return state;

}

public void setZip( String newZip ) {

zip = newZip;

}

public String getZip() {

return zip;

}

public void setEmail( String newEmail ) {

email = newEmail;

}

public String getEmail() {

return email;

}

public void setPhoneNumber( String newPhoneNumber ) {

phoneNumber = newPhoneNumber;

}

public String getPhoneNumber() {

return phoneNumber;

}

public CreditInfo getCreditInfo() {

return creditInfo;

}

public void setCreditInfo( CreditInfo newCreditInfo ) {

creditInfo = newCreditInfo;

}

}

 



Класс Emailer


Класс Emailer используется для отправки электронных сообщений клиенту. Эти сообщения могут содержать подтверждение заказа или информацию о доставке. В этом классе имеются две статические переменные, необходимые для его конфигурирования. Первая переменная — имя сервера SMTP (Simple Mail Transfer Protocol — простой протокол электронной почты), который используется данным магазином для отправки почты. Вторая переменная — электронный адрес, который будет указан в письмах клиенту в качестве обратного, то есть в поле From (От). В нашем примере мы используем формат, в котором указывается как название фирмы XMLGifts, так и ее электронный адрес (orders® xmlgifts.com). Прежде чем использовать код, приведенный на прилагаемом к нашей книге компакт-диске, вы, разумеется, должны заменить эти данные теми, которые фактически фигурируют в вашей системе. Имя сервера SMTP, используемого для пересылки электронной почты, можно найти в параметрах конфигурации вашей почтовой программы. В листинге 5.12 показано начало кода класса Emailer.

Листинг 5.12. Начало кода класса Emailer (Emailer.java)

package com.XmlEcomBook.Chap05;

import java.util.*;

import java.io.*;

import javax.mail.*;

import javax.mail.internet.*;

public class Emailer {

static final String host = "SMTP-HOST-NAME";

static final String from = "XMLGifts<orders@xmlgifts.com>";

Первый метод в этом классе используется для сообщения клиенту об отправке ему посылки с заказом. Хотя это подтверждение не является абсолютно необходимым, оно имеет большое значение, так как клиент, получив такое сообщение, будет уверен, что его заказ действительно выполняется, а также найдет ответы на возможные вопросы о доставке заказа. В этом методе после получения информации о пользователе из объекта класса Order мы вызываем служебный метод под названием <jetMessage, передавая ему в качестве параметра электронный адрес клиента. Метод getMessage создает объект Message с помощью интерфейса API JavaMail, о чем будет подробнее сказано далее в этом разделе. В объекте Message указывается тема сообщения и вносится текст, после чего письмо отправляется. Отправка письма осуществляется с помощью метода send объекта Transport из интерфейса API JavaMail. Этот метод создает простое сообщение (листинг 5.13), в котором указывается идентификатор заказа, чтобы клиент мог в дальнейшем на него ссылаться. Метод можно легко расширить, чтобы включить более детальную информацию о заказе.




Листинг 5.13. Метод sendConfiramtion (Emailer.java)

public static void sendConfirmation(Order order) {

try {

CustomerInfo cust = order.getCustomerInfo();

Message msg = getMessage( cust.getEmail() );

msg.setSubject("XMLGifts.com Order Confirmation");

msg.setText("Your order is being processed");

msg.setText("Your order number is:" + order.getId() );

Transport.send(msg);

}

catch (MessagingException mex) {

}

}

Метод sendShipped (листинг 5.14) аналогичен методу sendConfirmation, отличие касается только текста посылаемого сообщения. /



Листинг 5.14. Метод sendShipped (Emailer.java)

public static void sendShipped(String email, String orderId ) {

try {

Message msg = getMessage( email );

msg.setSubject("Your XMLGifts.com Order has shipped");

msg.setText("Order number " + orderId + " has shipped" );

Transport.send(msg);

}

catch (MessagingException mex) {

}

}

Метод getMessage используется другими методами класса для осуществления большинства действий, необходимых при работе с интерфейсом API JavaMail. Аргументом этого метода является электронный адрес клиента, которому посылается сообщение. В первую очередь в этом методе создается новый объект Session, а его значением становится имя сервера SMTP, которое, как вы помните, берется из статической переменной класса Emailег. Затем создается новый объект Message и устанавливаются значения полей, отведенных для адресов отправителя и получателя сообщения, а также для текущей даты (на которую можно впоследствии ссылаться как на дату отправки сообщения).

Интерфейс прикладных программ (API) JavaMail — это набор классов, которые моделируют систему электронной почты и являются стандартным расширением Java. Этот интерфейс можно использовать для получения и отправления сообщений с помощью стандартных почтовых протоколов. Более подробную информацию вы найдете по адресу http://java.sun.com/products/javamail, и оттуда же вы сможете бесплатно загрузить версию 1.2 этого интерфейса API. Метод getMessage (листинг 5.15) использует интерфейс JavaMail для создания нового объекта Message, в котором указаны адреса отправителя и получателя, а также текущая дата.



Листинг 5.15. Метод getMessage (Emailer.java)

static Message getMessage( String toEmail ) throws MessagingException {

Properties props = new Properties();

props.put("mail.smtp.host", host);

Session session = Session.getDefaultInstance(props, null);

session.setDebug(true);

Message msg = new MimeMessage(session);

msg.setFrom(new InternetAddress(from));

InternetAddress[] address = {new InternetAddress(toEmail)};

msg.setRecipients(Message.RecipientType.TO, address);

msg.setSentDate(new Date());

return msg;

}

}

 




Класс Fullfilment


В процесс выполнения заказа входит все, что должно быть сделано с товаром после оформления заказа. Обычно для этого нужно найти товар на складе, запаковать его и отправить клиенту. Мы рассмотрим несколько упрощенный вариант выполнения заказа; в нашем примере будет учитываться только сам процесс доставки.

В классе Fullfilment имеются поля для хранения информации о фирме, которая осуществляет доставку, и о типе услуг. Таким образом, мы можем доставлять товары с помощью различных почтовых фирм — например, Federal Express, UPS или почтовой службы US, а также выбирать различные типы услуг — срочную доставку на следующий день, через один или два дня или доставку в обычном режиме. Также нужно знать, сколько клиент должен заплатить за доставку. Эта сумма добавляется к стоимости товаров, что в результате дает полную стоимость заказа. Когда заказ наконец отправлен, ему должен быть присвоен некоторый идентификационный номер, а также следует записать дату отправки заказа — эти сведения потребуются, если от клиента поступят какие-либо вопросы, связанные с доставкой.

В классе Fullfilmerit (листинг 5.3) имеются методы setXxx и getXxx для каждого из полей. Имеется также один специальный метод, setShipperAndClass. Он позволяет задавать в одной строке и название почтовой фирмы, и тип услуг (для данного заказа) [Эту совокупность далее мы будем называть способом доставки. — Примеч. перев.]. Как вы увидите в разделе «JSP-страница Shippinglnfo», это упрощает обработку введенных пользователем данных (о предпочтительном способе доставки). В этом методе используется объект StringTokenizer, который разделяет строку на две части, затем первую часть заносит в поле shipper (почтовая фирма), а вторую — в поле class (тип услуг). Это сделано для того, чтобы упростить ввод указанных данных. Ниже мы рассмотрим страницу Shippinglnfo.jsp и увидим, как используется данный метод.

Листинг 5.3. Класс Fulfilment (Fullfilment.java)

package com.XmlEcomBook.Chap05;

import java.util.Date;


import java.util.StringTokenizer;

public class Fulfillment {

String shipper;

//UPS, Fedex, USPS, etc.

String shippingClass;

//Overnight, 2 Day, regular, frieght, etc.

double costToCustomer;

// How much the customer is charged for shipping

String trackingNumber = "NO_TRACKING_NUMBER";

String dateSent = "NOT_SENT_YET";

public Fulfillment() {

}

public void setShipper( String newShipper ) {

shipper = newShipper;

}

public String getShipper() {

return shipper;

}

public void setShipperAndClass( String shipperAndClass ) {

StringTokenizer st = new StringTokenizer( shipperAndClass );

if( st.hasMoreTokens() ) {

shipper = st.nextToken();

if( st.hasMoreTokens() ) {

shippingClass = st.nextToken();

}

}

}

public void setShippingClass( String newClass ) {

shippingClass = newClass;

}

public String getShippingClass() {

return shippingClass;

}

public void setTrackingNumber( String newNumber ) {

trackingNumber = newNumber;

}

public String getTrackingNumber() {

return trackingNumber;

}

public void setDateSent( String newDate ) { dateSent = newDate;

}

public String getDateSent() {

return dateSent;

}

public double getCostToCustomer() {

return costToCustomer;

}

public void setCostToCustomer( double newCost ) {

costToCustomer = newCost;

}

}

 




Класс Order


Класс Order служит в основном в качестве контейнера для всевозможных сведений, которые мы собираем при оформлении заказа. Сюда входит информация о клиенте, о доставке, номер кредитной карты клиента, перечень заказанных товаров и данные, присланные поставщиком услуг по обработке (в частности, код подтверждения). Нам также понадобится уникальный идентификатор этого заказа, чтобы на него можно было впоследствии ссылаться. Кроме того, нужно указать дату заказа. Все эти сведения вносятся в соответствующие поля класса Order. Конструктор класса задает идентификатор и дату заказа. Метод, используемый для генерации уникального идентификатора, мы исследуем несколько позже. В листинге 5.5 показаны методы getXxx и setXxx для полей класса. Для полей id и date методы setXxx отсутствуют, так как их значения задаются конструктором класса и затем не меняются.

Листинг 5.5. Поля, конструктор и методы setXxx и getXxx для класса Order (Order.java)

package com.XmlEcomBook.Chap05;

import com.XmlEcomBook.catalog.CartItem;

import java.util.*;

import java.io.*;

import com.XmlEcomBook.util.Debug;

public class Order {

private int id; //unique id for this order

private Date date; //date of order

private Vector items = new Vector();

private CustomerInfo customerInfo;

private Authorization authorization; //payment authorization

private Fulfillment fulfillment;

public Order() {

id = getUniqueId();

date = new Date();

}

public int getId() {

return id;

}

public Date getDate() {

return date;

}

public void setItems( Vector newItems ) {

if( newItems != null )

items = newItems;

}

public Vector getItems() {

return items;

}

public void setCustomerInfo( CustomerInfo newCustomer ) {

if( newCustomer != null )

customerInfo = newCustomer;

}

public CustomerInfo getCustomerInfo() {

return customerInfo;

}

public void setAuthorization( Authorization newAuth ) {

authorization = newAuth;

}

public Authorization getAuthorization() {

return authorization;

}

public void setFulfillment( Fulfillment newFulfillment ) {


fulfillment = newFulfillment;

}

public Fulfillment getFulfillment() {

return fulfillment;

}

В листинге 5. 6 показаны некоторые методы, оперирующие данными, которые содержатся в классе Order. Первый метод, getTotalltemPrice, реализует цикл по всем заказанным товарам и для каждого товара умножает его цену на количество заказанных экземпляров. Затем подсчитывается и возвращается общая сумма. Метод getOrderTotal добавляет к общей сумме стоимость доставки, что и составляет общую стоимость заказа. Наконец, имеется метод getTotalltemWeight, который аналогичен методу getTotalltemPrice, но только в нем подсчитывается не стоимость, а общий вес заказа. Метод getPrice-осуществляет вспомогательные функции — удаляет дополнительные символы (знак $ и запятые) из строки с указанием цены, полученной из объекта Cartltem, а затем преобразует полученное число к типу doubl e.



Листинг 5.6. Методы для подсчета характеристик заказа как единого целого (Order.java)

public double getTotalItemPrice() {

double total = 0;

Enumeration enum = items.elements();

while( enum.hasMoreElements() ) {

CartItem item = (CartItem)enum.nextElement();

total += getPrice( item ) * item.getNumberOrdered();

}

return total;

}

public double getTotalItemWeight() {

double total = 0;

Enumeration enum = items.elements();

while( enum.hasMoreElements() ) {

CartItem cartItem = (CartItem)enum.nextElement();

double d = Double.parseDouble(cartItem.getShippingValue());

total += d * cartItem.getNumberOrdered();

}

return total;

}

private double getPrice( CartItem item ) {

String s = item.getPrice();

//remove dollar sign

s = s.replace( '$', ' ' );

//remove commas

int i;

while( (i = s.indexOf( ',' )) > 0 ) {

s = s.substring( 0, i ) + s.substring( i + 1 );

}

return Double.parseDouble( s );

}

Имеется также метод для записи заказа в формате XML. Информация о заказе может пригодиться в будущем. Поэтому мы создали определение DTD, которое является непосредственным отображением класса Order и всех классов, которые в нем используются. В листинге 5.7 представлен файл order.dtd, в котором каждому из полей класса Order (item, customer-Info, authorization и fullfilment) соответствует элемент, дочерний по отношению к элементу order. Поля id и date класса Order представлены атрибутами элемента order. Таким же образом сформированы и остальные элементы DTD: каждому полю класса Order соответствует элемент или атрибут в DTD [Order — заказ, item — товар, first name — имя, last name — фамилия, address — адрес, city — город, state — штат или страна, zip — почтовый индекс, phone — номер телефона, card number — номер кредитной карты, card type — тип кредитной карты, expiration date — дата окончания срока действия, reason — причина (отклонения кредитной карты), authorization code — код подтверждения, cost — стоимость, tracking number — номер для отслеживания заказа, date sent — дата отправки. — Примеч. перев. ].





Листинг 5.7. DTD для описания структуры заказа (order.dtd)

<!ELEMENT order (item*, customer_info, authorization, fulfillment )>

<!ATTLIST order id ID #REQUIRED

date CDATA #REQUIRED>

<!ELEMENT item (#PCDATA)>

<!ATTLIST item id NMTOKEN #REQUIRED

quantity NMTOKEN #REQUIRED

price CDATA #REQUIRED>

<!ELEMENT customer_info (first_name, last_name, address1, address2,

city, state, zip, email, phone, credit_info )>

<!ELEMENT first_name (#PCDATA)>

<!ELEMENT last_name (#PCDATA)>

<!ELEMENT address1 (#PCDATA)>

<!ELEMENT address2 (#PCDATA)>

<!ELEMENT city (#PCDATA)>

<!ELEMENT state (#PCDATA)>

<!ELEMENT zip (#PCDATA)>

<!ELEMENT email (#PCDATA)>

<!ELEMENT phone (#PCDATA)>

<!ELEMENT credit_info (card_numer, card_type, expiration )>

<!ELEMENT card_number (#PCDATA)>

<!ELEMENT card_type (#PCDATA)>

<!ELEMENT expiration_date (#PCDATA)>

<!ELEMENT authorization (reason?, auth_code?)>

<!ATTLIST authorization approved CDATA #IMPLIED>

<!ELEMENT reason (#PCDATA)>

<!ELEMENT authorization_code (#PCDATA)>

<!ELEMENT fulfillment (backorder_date, shipper, shipping_class,

cost, tracking_number, date_sent)>

<!ELEMENT shipper (#PCDATA)>

<!ELEMENT shipping_class (#PCDATA)>

<!ELEMENT cost (#PCDATA)>

<!ELEMENT tracking_number (#PCDATA)>

<!ELEMENT date_sent (#PCDATA)>

В листинге 5.8 приведен метод, который создает документ XML, соответствующий этому определению DTD. Это примитивный, слишком прямолинейный метод, в котором просто перебираются все поля класса Order и для каждого поля выписываются элементы и атрибуты XML. Такой подход не всегда является оптимальным, так как изменения в классах, входящих в класс Order, приведут к необходимости изменения самого метода. Таким образом, теряются преимущества инкапсуляции, которую обеспечивает объектно-ориентированный подход к программированию. Было бы лучше рассматривать эти классы в единстве. Более удачный метод создания документа XML мы рассмотрим в главе 6.





Листинг 5.8. Метод writeXML (Order.java)

public void writeXML( Writer writer ) {

try {

writer.write( "<?xml version='1.0' ?>" );

writer.write( "<!DOCTYPE order SYSTEM '.."

+ File.separator + "order.dtd'>" );

writer.write( "<order id='" + id + "' " );

writer.write( "date='" + date + "'>" );

Enumeration enum = items.elements();

while( enum.hasMoreElements() ) {

CartItem item = (CartItem)enum.nextElement();

writer.write( "<item id='" + item.getId() + "' " );

writer.write( "quantity='" + item.getNumberOrdered() + "' ");

writer.write( "price='" + item.getPrice() + "'>" );

writer.write( item.getName() + "</item>" );

}

writer.write( "<customer_info>\n<first_name>" +

customerInfo.getFirstName() + "</first_name>" +

"\n<last_name>" + customerInfo.getLastName() + "</last_name>" +

"\n<address1>" + customerInfo.getAddress1() + "</address1>" +

"\n<address2>" + customerInfo.getAddress2() + "</address2>" +

"\n<city>" + customerInfo.getCity() + "</city>" +

"\n<state>" + customerInfo.getState() + "</state>" +

"\n<zip>" + customerInfo.getZip() + "</zip>" +

"\n<email>" + customerInfo.getEmail() + "</email>" +

"\n<phone>" + customerInfo.getPhoneNumber() + "</phone>" );

CreditInfo credit = customerInfo.getCreditInfo();

writer.write( "\n<credit_info>\n<card_number>" +

credit.getCreditCardNumber() + "</card_number>" +

"\n<card_type>" + credit.getCreditCardType() + "</card_type>" +

"\n<expiration_date>" + credit.getExpirationDate() +



"</expiration_date>\n</credit_info>\n</customer_info>" );

writer.write( "\n<authorization approved='" +

authorization.isApproved() + "'>" +"\n<reason>" +

authorization.getReason() + "</reason>\n<authorization_code>" +

authorization.getAuthorizationCode() + "</authorization_code>" +

"</authorization>" );

writer.write( "\n<fulfillment>\n" +

"\n<shipper>" + fulfillment.getShipper() + "</shipper>" +

"\n<class>" + fulfillment.getShippingClass() + "</class>" +

"\n<cost>" + fulfillment.getCostToCustomer() + "</cost>" +

"\n<tracking_number>" + fulfillment.getTrackingNumber() +

"</tracking_number>\n<date_sent>" +

fulfillment.getDateSent() + "</date_sent>\n</fulfillment>" );

writer.write( "\n</order>" );

}

catch( IOException e ) {

}

}

Последний метод в классе Order, названный getllniqueld, приведен в листинге 5.9. Он генерирует уникальный идентификатор заказа. В нашем случае мы используем счетчик, который увеличивается на единицу каждый раз, когда нам требуется новый идентификатор. Этот счетчик должен храниться постоянно, чтобы мы могли быть уверенны в уникальности присваиваемых идентификаторов даже после полной перезагрузки системы. Хранение счетчика в памяти — в данном случае не выход из положения, так как при каждой перезагрузке системы счетчик начинает повторять уже присвоенные ранее идентификаторы. В качестве постоянного места хранения счетчика мы используем специальный файл.



Листинг 5.9. Метод getUniqueld (Order.java)

synchronized private int getUniqueId() {

int id;

try {

ObjectInputStream in = new ObjectInputStream( new

FileInputStream( "orderID.txt" ) );

id = in.readInt();

}

catch( IOException e ) {

id = 1000;

}

try {

ObjectOutputStream out = new ObjectOutputStream( new

FileOutputStream( "orderID.txt" ) );

out.writeInt( id + 1 );

out.close();

}

catch( IOException e ) {

}

return id;

}

}

 




Класс ShippingCalculator


Теперь нам нужен какой-нибудь способ подсчета стоимости доставки товара заказчику. Эта сумма зависит от нескольких факторов, главными из которых являются выбранная клиентом почтовая фирма и тип услуг. На стоимость также могут влиять вес посылки и расстояние до пункта назначения. Мы при вычислении общей стоимости будем учитывать только три фактора: почтовую фирму, тип услуг и вес посылки. Объекту ShippingCalculator в качестве параметра передается объект Order, в котором содержится вся необходимая информация о заказе: вес посылки и адрес пункта назначения.

Первый метод, getTypes, возвращает массив, в котором перечислены все типы услуг, предоставляемые каждой почтовой фирмой, сотрудничающей с магазином. Второй метод, getPrice, возвращает цену конкретного типа доставки конкретной фирмой. Входным параметром этого метода должна быть одна из строк массива, возвращенного методом getTypes.

Метод getPrice (листинг 5.11) можно расширить, чтобы учитывать при вычислении стоимости доставки большее количество факторов, чем перечисленные выше (вес посылки и способ доставки). Кроме того, было бы неплохо организовать считывание информации в этот класс из некоторого источника данных, например файла XML или базы данных, с последующим вычислением стоимости. Это позволило бы динамически менять указанные цифры в соответствии с реальными колебаниями цен, не меняя самого кода.

Листинг 5.11. Начало кода класса ShippingCalculator и его конструктора (ShippingCalculator.java)

public class ShippingCalculator {

Order order;

public ShippingCalculator( Order setOrder ) {

order = setOrder;

}

public String[] getTypes() {

String[] names = { "FedEx Overnight",

"FedEx 2-Day",

"UPS Overnight",

"UPS 3-Day",

"USPS 2-Day",

"USPS Regular" };

return names;

}

public String getPrice( String name ) {

double weight = order.getTotalItemWeight();

if( name.equals( "FedEx Overnight" ) )

if( weight > 3.0 )

return "$10.99";

else

return "$7.99";

if( name.equals( "FedEx 2-Day" ) )

if( weight > 3.0 )

return "$5.99";

else

return "$3.49";

if( name.equals( "UPS Overnight" ) )

if( weight > 2.0 )

return "$8.99";

else

return "$6.99";

if( name.equals( "UPS 3-Day" ) )

if( weight > 2.5 )

return "$5.99";

else

return "$4.99";

if( name.equals( "USPS 2-Day" ) )

if( weight > 2.5 )

return "$4.99";

else

return "$3.99";

if( name.equals( "USPS Regular" ) )

return "$2.99";

return "0.00";

}

}

 



Класс TestPaymentAuthorizer


Следующие классы, которые мы будем рассматривать, используются в процессе оформления заказа. Первый из них занимается проверкой данных о кредитной

карте клиента. В этом примере мы создали очень простой класс, который возвращает код подтверждения в зависимости от последней цифры номера кредитной карты, содержащегося в классе Order. В настоящем электронном магазине вместо этого класса должен быть подставлен другой, который передает данные о кредитной карте поставщику услуг по обработке. Способ передачи этих данных в большой степени зависит от конкретного поставщика.

Класс TestPaymentAuthori zer представлен в листинге 5.10. Первый метод в этом классе, getAuthorization, в качестве параметра получает объект Order, из которого извлекается информация о кредитной карте для проверки. В нашем примере мы просто берем последнюю цифру номера и, если это 1, карта с таким номером отклоняется, то есть считается не прошедшей проверку. Этот абстрактный, произвольный критерий мы используем только для того, чтобы протестировать работу метода и продемонстрировать оба типа возвращаемых значений.

Метод capture нужен для того, чтобы завершить процесс снятия денег со счета клиента, осуществляемый поставщиком услуг по обработке. В нашем случае этот метод ничего не делает, но, опять-таки, в настоящем магазине он использовался бы для сообщения поставщику о том, что следует завершить процесс оплаты. Если бы мы хотели автоматизировать процесс возвращения денег в случае, когда клиент возвращает товар, или процесс разблокировки счета клиента, если заказанного товара не оказалось в наличии, то соответствующие методы следовало бы тоже поместить в класс TestPaymentAuthori zer.

Листинг 5.10. Класс TestPaymentAuthorizer (TestPaymentAuthorizer.java)

public class TestPaymentAuthorizer {

static public Authorization getAuthorization( Order order ) {

Authorization authorization = new Authorization();

try {

CustomerInfo custInfo = order.getCustomerInfo();

CreditInfo creditInfo = custInfo.getCreditInfo();

String num = creditInfo.getCreditCardNumber();

if( num != null ) {

if( num.endsWith( "1" ) ) {

authorization.setApproved( false );

authorization.setReason("Insufficent Funds");

}

else

{

authorization.setApproved( true );

authorization.setReason( "Approved" );

authorization.setAuthorizationCode( "Test" );

}

}

}

catch( Exception e ) {}

return authorization;

}

static public void capture( String authorizationCode, double amount ) {

}

}



Обновление информации о доставке


После того как клиент ввел и подтвердил информацию о заказе, отдел доставки электронного магазина должен иметь возможность просматривать эту информацию и обновлять значения таких характеристик, как номер заказа и дата отправки заказа клиенту. Мы также предложим способ формирования списка всех заказов, поступивших в конкретный день, и выбора из этого списка определенного заказа.



Процесс оплаты


Для коммерческих web-сайтов, занимающихся розничной продажей, прием платежей через кредитные карты является общепринятой практикой. Вообще говоря, оплата по кредитной карте товаров, приобретенных как непосредственно в магазине, так и через систему «товары почтой», появилась на много лет раньше, .чем WWW. На розничном уровне электронной торговли сложилась определенная последовательность действий, необходимая для оплаты через кредитные карты, и каждый электронный магазин работает с кредитными картами на основе этой последовательности. Прежде чем исследовать подробности ее реализации, рассмотрим, из каких этапов она складывается.

В процессе оплаты товара кредитной картой участвует несколько действующих лиц: сам клиент, магазин, банк клиента, поставщик услуг по обработке и поставщик коммерческих услуг. Роли клиента и магазина в процессе оплаты товара более или менее ясны. Банк клиента — это финансовое учреждение, которое выдало ему кредитную карту. В следующих двух абзацах описываются роли двух остальных действующих лиц.

Поставщик коммерческих услуг магазина часто является банком, но может быть и другим финансовым институтом; например, в этом качестве может выступать сама компания, выпускающая кредитные карты. Чтобы работать с кредитными картами, магазин должен заключить с банком соглашение. Требования и условия у каждого банка свои, но, как правило, взимаются по крайней мере три вида платежей. Во-первых, имеется так называемая учетная ставка (discount rate), размер которой обычно колеблется от 2,5 до 5 %. Это процент, отчисляемый от каждого платежа, который проходит через коммерческого поставщика. Во-вторых, при прохождении каждого платежа отчисляется некоторая небольшая фиксированная сумма, обычно в пределах от 30 до 50 центов (transaction fee). Наконец, коммерческий банк обычно взимает за свои услуги некоторую ежемесячную оплату. Магазин и банк должны договориться обо всех этих платежах, прежде чем магазин начнет принимать к оплате кредитные карты.


Наконец, пятым действующим лицом в этом процессе является поставщик услуг по обработке. Другое название этого поставщика — расчетная палата (clearinghouse). В его обязанности входит прием запросов от магазина, проверка достоверности полученных от клиентов сведений, проверка наличия на счетах покупателей достаточных средств и, наконец, перевод денег между банками.

Фактически процесс оплаты кредитной картой состоит из двух этапов. Сначала магазин посылает поставщику услуг по обработке запрос о проверке сведений, поступивших от клиента. В этом запросе указываются сумма покупки, номер кредитной карты и адрес клиента. Поставщик проверяет существование указанного счета в банке, наличие на счету клиента достаточной суммы и, возможно, адрес владельца кредитной карты. Если все в порядке, то поставщик высылает магазину код подтверждения. На этом этапе не происходит никакой передачи денег, но на счету клиента блокируется сумма, равная сумме заказа, сделанного им в электронном магазине.

Следующий этап процесса обработки (этап, когда деньги фактически снимаются со счета клиента) начинается после того, как магазин закончил всю деятельность по выполнению заказа. Поставщику услуг по обработке посылается второе сообщение. В реальном магазине (в отличие от виртуального) этот этап начинается после того, как покупатель вышел из магазина со своей покупкой. Обычно в таком магазине все чеки после завершения рабочего дня (или смены) собираются и отсылаются вместе. Но чек, в принципе, может быть послан и отдельно. В электронном магазине снятие денег со счета клиента происходит не ранее чем ему фактически отправлен товар. Это предотвращает вероятность слишком раннего снятия денег со счета клиента, например в случае, если товар на складе временно отсутствует.

Помимо подтверждения правильности сведений о кредитной карте и фактического снятия денег со счета клиента поставщик услуг по обработке осуществляет и другие функции, например расторжение сделки. Например, если после завершения первого этапа оказывается, что по какой-либо причине заказ не может быть выполнен (например, товар отсутствует на складе или клиент раздумал его покупать), осуществляется расторжение сделки, то есть снимается блокировка с соответствующей суммы денег на счету клиента.



Еще один вид услуг, выполняемых этим поставщиком, — возвращение денег клиенту. Если клиент по какой-либо причине возвращает поставщику товар, оплаченный по кредитной карте, то деньги за возвращенный товар снова заносятся на счет клиента.

Для небольших web-магазинов, возможно, не имеет смысла пытаться полностью автоматизировать описанный процесс. Если количество заказов невелико, то может оказаться более экономичным выполнять все описанные действия «вручную». При этом проверка кредитных карт клиентов, вероятно, займет несколько более длительное время, но в целом такое решение может оказаться предпочтительным для небольших электронных магазинов, которые только начинают свою деятельность.

Существует множество компаний, предлагающих услуги по организации всего описанного процесса электронным магазинам, которые хотели бы автоматизировать обработку электронных платежей. Эти компании являются либо поставщиками услуг по обработке, либо посредниками между такими поставщиками и электронными магазинами. Возможности и условия этих компаний сильно различаются, так что перед тем, как сделать выбор, вам предстоит изучить множество предлагаемых вариантов. Можно порекомендовать такие компании, как CyberSource (www.cybersource.com), Verifone (www.venfone.com), Authonze.net (www.authonze.net) и Clear-Commerce (www.clearcommerce.com). Большая часть этих компаний предлагают не только услуги по обработке, но и установку соответствующего программного обеспечения на web-сайт поставщика коммерческих услуг, что упрощает взаимодействие с ним.

 




Сбор информации о заказе


Когда покупатель отобрал товары, которые он собирается приобрести, следующим этапом является подсчет стоимости покупки и выполнение заказа. Необходимо получить определенную информацию о покупателе, в том числе его имя, адрес и номер телефона или адрес электронной почты (или установить какой- нибудь другой способ связи с ним). Также покупатель может выбрать один из способов доставки товара. Наконец, самое главное — покупатель должен сообщить номер своей кредитной карты, которой он будет оплачивать покупку.

Мы начнем с изучения классов, которые будут использоваться в JSP-страни- це как компоненты JavaBean. Customerlnfo, Creditlnfo, Fulfillment, Authorization и Order — все это классы, которые содержат информацию, собранную в процессе оформления заказа.

Следующие три класса, которые мы изучим, — TestPaymentAuthorizer, Ship- pi ngCal cul ator и Eraai 1 er — также используются в процессе оформления заказа. Затем мы рассмотрим HTML-страницу, сервлет и несколько JSP-страниц, с помощью которых осуществляется взаимодействие с пользователем при сборе необходимой информации.



Сервлет SubmitOrder


Когда подтвержденные клиентом данные (то есть JSP-страница Confirmlnfo) отправлены, необходимо послать поставщику услуг по обработке сведения, связанные с оплатой заказа. Это делается с помощью сервлета SubmitOrder. Здесь мы используем не JSP-страницу, а сервлет, поскольку данная процедура, с одной стороны, требует довольно много кода, а с другой стороны, генерирует сравнительно немного выходных данных. Дело в том, что, как правило, создавать и отлаживать сервлеты проще, чем JSP-страницы, но отрицательной стороной сер- влетов является сложность генерируемого кода HTML.

Главная точка входа в сервлет — метод doGet, который получает объект Session, соответствующий текущему сеансу (листинг 5.24). Затем мы получаем объект Order, сформированный для этого сеанса предыдущими JSP-страницами. Далее для получения подтверждения данных кредитной карты используется класс TestPaymentAuthorizer. Если подтверждение получено, клиенту посылается соответствующее сообщение с помощью класса Emailег, вызывается метод, который записывает заказ в специальный файл, и вызывается JSP-страница Approved.jsp, создающая сообщение с информацией о подтверждении заказа и указанием идентификатора заказа. Если подтверждение не было получено, вызывается JSP-страница Declined .jsp и создается другое сообщение.

Листинг 5.24. Метод doGet сервлета SubmitOrder (SubmitOrder.java)

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.XmlEcomBook.Chap05.*;

// Explain why we used a servlet: easier to code, debug

public class SubmitOrder extends HttpServlet {

public void doGet(HttpServletRequest req,

HttpServletResponse res)

throws IOException, ServletException {

res.setContentType("text/html");

PrintWriter out = res.getWriter();

try {

HttpSession session = req.getSession();

Order order = (Order)session.getAttribute( "order" );

Authorization auth = TestPaymentAuthorizer.getAuthorization(order );

order.setAuthorization( auth );


if( auth.isApproved() ) {

//Emailer.sendConfirmation( order );

writeOrder( order );

getServletContext()

.getRequestDispatcher("/Approved.jsp").forward(req, res);

}

else {

getServletContext()

.getRequestDispatcher("/Declined.jsp").forward(req, res);

}

}

catch( Exception e )

{

e.printStackTrace(out); } }

Метод writeOrder используется для записи информации о заказе в файл XML. Этот метод помещает все файлы с заказами, поступившими в течение одного дня, в отдельную папку. Поэтому для определения даты (год, месяц и число месяца) создается специальный объект, GregorianCalendar. В имени папки указывается соответствующая дата, а название файла содержит идентификатор заказа. Метод mkdir класса File вызывается для проверки наличия данной папки, а затем с помощью метода writeXML объекта Order информация о заказе заносится в файл, как показано в листинге 5.25.



Листинг 5.25. Метод writeOrder (SubmitOrder.java)

private void writeOrder( Order order ) {

try

{

Calendar calendar = new GregorianCalendar();

int day = calendar.get( Calendar.DAY_OF_MONTH );

int month = calendar.get( Calendar.MONTH ) + 1;

int year = calendar.get( Calendar.YEAR );

String dir = "Orders_" + year + "-" + month + "-" + day;

String filename = "Order_" + order.getId() + ".xml";

File file = new File( dir );

file.mkdir();

FileWriter writer = new FileWriter( dir + File.separator + filename );

order.writeXML(writer);

writer.close();

}

catch( IOException e ) {

}

}

}

 




Страница Customerlnfo


Теперь мы перейдем к классам, с помощью которых осуществляется получение всей информации от клиента и передача ее рассмотренным выше классам. В первую очередь — это HTML-страница Customerlnfo.html. На этой странице имеется форма, поля которой предназначены для введения информации пользователем и соответствуют полям класса Customerlnfo (листинг 5.16). Как будет видно на JSP-странице Shippinglnfo, это соответствие упрощает передачу данных из формы в класс.

Листинг 5.16. Страница Customerlnfo (Customerlnfo.html)

<html>

<head>

<title>Customer Info</title>

</head>

<body>

<form action="ShippingInfo.jsp">

<p>

First Name:<input name="firstName" />

Last Name:<input name="lastName" />

</p>

<p>

Address line 1:<input name="address1" /><br />

Address line 2:<input name="address2" /><br />

City:<input name="city" />

State:<input size="2" name="state" />

Zip:<input size="10" name="zip" />

</p>

<p>Email Address:<input name="email" /><br /></p>

<p>Phone Number:<input size="13" name="phoneNumber" /> <br /></p>

<input type="submit" value="Submit Information" />

</form>

</body>

</html>

 



Главная HTML-страница


На главной HTML-странице пользователю предлагаются три возможности: добавить товар, редактировать товар или удалить товар. Существует также возможность отменить все изменения, сделанные пользователем в текущем сеансе, или сохранить эти изменения. Для всех этих операций вызывается один и тот же сервлет, но всякий раз с различными значениями скрытого поля с именем operation. В начале HTML-страницы (листинг 6.41) содержится ее заголовок и открывающий тег элемента body.

Листинг 6.41. Начало HTML-страницы (main.html)

<html>

<head><title>Catalog Upkeep</title></head>

<body>

Каждая из пяти операций, предложенных пользователю, представлена специальной формой на странице. Первую операцию, Add Product (Добавить товар), иллюстрирует листинг 6.42. Для нового товара требуется уникальный идентификатор, поэтому нужно проверить все уже существующие идентификаторы, чтобы не получилось повторения. Также требуется указать серию товаров, к которой принадлежит новый товар. Атрибут action элемента form содержит имя сервлета, который вызывается, когда форма заполнена и отправлена на сервер. В данном случае мы вызываем сервлет Main. В следующей строке указывается заголовок для формы Add Product.

Первый элемент этой формы является скрытым. Это значит, что пользователь не увидит его на экране своего компьютера. Он просто передается сервлету вместе со всеми остальными введенными данными. Этот элемент позволяет сервлету определить, что именно хотел сделать пользователь. Следующие две строки предназначены для того, чтобы пользователь мог ввести идентификатор товара и указать серию товаров, к которой он будет принадлежать. Последняя строка элемента form нужна для того, чтобы добавить кнопку, с помощью которой пользователь сможет отправить введенные данные на сервер.

Листинг 6.42. Форма для добавления товара (main.html)

<form action="servlet/Main">

<h3>Add Product:</h3>

<input type="hidden" name="operation" value="add" />


Product ID:<input name="productid" />

Product Line:<input name="productline" />

<input type="submit" value="Add"/> </form>

Формы Delete Product (Удалить товар) и Edit Product ( Редактировать товар) аналогичны форме Add Product, но в них требуется ввести только идентификатор товара. Для визуального разграничения этих форм между ними добавляется горизонтальная линия. Код для форм Delete Product и Edit Product приведен в листинге 6.43.

Листинг 6.43. Формы Delete Product и Edit Product (main.html)

<hr />

<form action="servlet/Hain">

<h3>Delete Product:</h3>

<input type="hidden" name="operation" value="delete" />

Product ID:<input name="productid" />

<input type="submit" value="Delete"/>

</form>

<hr />

<form action="servlet/Main"> <h3>Edit Product:</h3>

<input type="hidden" name="operation" value="edit" />

Product ID:<input name="productid" />

<input type="subrait" value="Edit"/>

</form>

Последние две операции — отмена и сохранение всех изменений, внесенных пользователем в течение данного сеанса, — иллюстрирует листинг 6.44. Сервлет и JSP-страница загружают данные XML из файла, когда пользователь начинает работу с каталогом. Форма Cancel All Changes (Отменить все изменения) отменяет все изменения, которые были сделаны с момента загрузки файла. Форма Save All Changes (Отменить все изменения) записывает в файл XML все изменения, сделанные пользователем. Визуально обе эти формы представляют собой просто кнопки, на которых может щелкнуть пользователь. В каждой форме имеется скрытый элемент input, который сообщает сервлету, какую из этих операций выбрал пользователь.

Листинг 6.44. Формы Cancel All Changes и Save All Changes (main.html)

<hr />

<form action="servlet/Main">

<input type="hidden" name="operation" value="refresh" />

<input type="submit" value="Cancel All Changes"/>

</form>

<form action="servlet/Main">

<input type="hidden" name="operation" value="save" />

<input type="submit" value="Save All Changes"/>

</form>

</body> </html>




JSP-страница Delete


Страница Del ete, представленная в листинге 6.49, устроена довольно просто. Вы получаете идентификатор товара из встроенного объекта request. Это тот же самый объект, который был адресован JSP-странице сервлетом Main. Затем вы получаете объект catal од из текущего сеанса. Нужно просто удалить товар из каталога и отобразить HTML-страницу с сообщением о том, что удаление прошло успешно.

Листинг 6.49. JSP-страница Delete (Delete.jsp)

<%@ page import="com.XmlEcomBook.Chap06.*" %> <% String pid = request.getParameter( "productid" ); Catalog catalog = (Catalog)session.getValue( "catalog" ); catalog.deleteProduct( pid ); %>

<html> <head> <title>Delete</title> </head> <body> <p><b>Product <%= pid %> deleted.<b></p> <a href='/main.html'>Return to main page.<a> </body> </html>



JSP-страница Edit


Страница Edit гораздо длиннее и сложнее, чем страница Delete. Она должна генерировать форму, в которой содержатся все данные о товаре в таком виде, который позволяет пользователю редактировать эти данные. В верхней части страницы содержится несколько элементов сценария. Как видно из листинга 6.50, после импорта необходимых пакетов из объекта request извлекаются требуемые параметры. Это те же самые параметры, которые были введены пользователем и затем переданы JSP-странице сервлетом Main.

Эта JSP-страница выполняет команды добавления и редактирования сервле- та Main, поэтому мы должны проверить параметр operation, указывающий, какая из двух операций будет выполняться. Если значение этого параметра равно edit, то мы знаем, что нужно извлечь указанный элемент из каталога. Если значение его не равно edit, очевидно, оно равно add; тогда нужно создать новый элемент каталога и установить его идентификатор равным тому значению, которое введено пользователем.

Листинг 6.50. Начало JSP-страницы Edit (Edit.jsp)

<%@ page import="com.XmlEcomBook.Chap06.*" %> <%@ page import="java.util.*" %> <% String pid = request.getParameter( "productid" ); String operation = request.getParameter( "operation" ); String productLine = request.getParameter( "productline" ); Catalog catalog = (Catalog)session.getValue( "catalog" ); Product product = null; String name = ""; if( operation.equals( "edit" ) ) { product = catalog.getProduct( pid ); name = "Edit"; } else { //it’s an “add” operation product = new Product(); product.setId( pid ); name = "Add"; } %>

Далее начинается фактическое формирование HTML-страницы, как показано в листинге 6.51. Сначала нужно установить заголовок страницы в тегах <head> и <hl>, после чего следует элемент form, содержащий остальную часть страницы. Элемент form вызывает сервлет с именем UpdateProduct, который мы обсудим в следующем разделе. Первые два элемента input этой формы — скрытые поля, содержащие информацию, необходимую для сервлета UpdateProduct. Это введенные пользователем идентификатор товара и серия товаров, к которой он относится. Название товара, серия и идентификатор вставляются в выходные данные JSP-страницы с помощью выражения (элемента сценария), которое начинается с символов <*=.


Листинг 6.51. Начало кода элементов HTML-страницы (Edit.jsp)

<html> <head><title><%= name %></title></head> <body> <h1><%= name %> Product</h1> <form action="/servlet/UpdateProduct"> <input name="productline" type="hidden" value="<%= request.getParameter( "productline" ) %>" /> <input name="id" type="hidden" value="<%= product.getId() %>" />

Далее начинается элемент table, который помогает выровнять строки, входящие в форму, как показано в листинге 6.52. Первый элемент, который вы отображаете, — это идентификатор товара. Он просто выводится в первой строке таблицы table. Название товара отображается в той же строке таблицы, что и его идентификатор, но в виде отдельного текстового поля, состоящего из одной строки, — этот тип для объекта input задается по умолчанию. Служебный метод notNull используется потому, что метод getName может выдать значение null, а мы хотели бы, чтобы в таких случаях отображалась пустая строка, а не строка "null". В следующей строке таблицы отображаются ключевые слова, характеризующие данный товар. Размер этого текстового поля установлен равным 40, потому что предполагается, что вводимая строка может быть несколько длиннее

Листинг 6.52. Отображение названия товара и ключевых слов (Edit.jsp)

<table> <tr> <td>Product ID</td> <td><%= product.getId() %></td> <td>Name</td> <td><input name="name" value="<%= Util.notNull(product.getName()) %>" /></td> </tr> <tr> <td>Keywords</td> <td colspan="3"><input size ="40" name="keywords" value="<%= Util.notNull(product.getKeywords()) %>" /></td> </tr>

Далее, у нас имеется два скриптлета, представленные в листинге 6.53, которые выводят для товара элементы Author и Artist. Первый скриптлет осуществляет цикл по всем элементам Author для данного товара и использует метод outputAuthor для их отображения. Для идентификации каждого автора (элемента Author) счетчик цикла преобразуется в строку путем добавления его значения к пустой строке. Затем формируется пустой элемент Author с идентификатором New (новый). Это позволяет пользователю дополнить список авторов, введя новый элемент Author. Второй скриптлет выполняет такие же действия для элементов Arti st. Код методов outputAuthors и outputArti sts мы рассмотрим ниже в этом разделе.



Листинг 6.53. Отображение элементов Author и Artist (Edit.jsp)

<% Enumeration authors = product.getAuthors(); for( int i = 0; authors.hasMoreElements(); i++ ) { out.print( outputAuthor( "" + i, (String)authors.nextElement() ) ); } out.print( outputAuthor( "New", "" ) ); %> <% Enumeration artists = product.getArtists(); for( int i = 0; artists.hasMoreElements(); i++ ) { out.print( outputArtist( "" + i, (String)artists.nextElement() ) ); } out.print( outputArtist( "New", "" ) ); %>

Следующая часть JSP- страницы выводит значения цены, количество экземпляров данного товара в магазине и дату начала продаж, а также описание товара, как показано в листинге 6.54. Каждому из этих элементов отводится своя строка в таблице, причем каждая строка содержит два элемента данных — идентификатор данного поля ввода и само поле ввода. Поскольку описание может содержать большой текстовый фрагмент, для него используется элемент textarea (текстовое поле с несколькими строками) вместо одиночной строки элемента input.

Листинг 6.54. Отображение цены, количества экземпляров, даты начала продаж и описания товара (Edit.jsp)

<tr><td>Price</td> <td><input name="price" value="<%= product.getPrice() %>" /> </td> <td>Discount</td> <td><input name="discount" value="<%= Util.notNull(product.getDiscount()) %>" /></td> </tr> <tr><td>Quantity in Stock</td> <td><input name="quantity" value="<%= product.getQuantityInStock() %>" /></td> </tr> <tr><td>On Sale Date</td> <td colspan="3"><input name='onSaleDate' value='<%= product.getOnSaleDate() %>' /> (mm-dd-yyyy hh:mm:ss)</td> </tr> <tr><td>Description</td> <td colspan="3"> <textarea rows="5" cols="40" name="description"><%= Util.notNull(product.getDescription()) %> </textarea> </td> </tr> </table>



Для отображения элементов Image и Clip, относящихся к данному товару, за- действуется новая таблица, задаваемая в листинге 6.55. Скриптлеты в данном случае аналогичны тем, которые использовались для отображения элементов Author и Artist. После отображения таблицы в нижней части страницы появляется кнопка Submit (Принять), на которой после завершения редактирования пользователь может щелкнуть для отправки содержимого формы на сервер.

Листинг 6.55. Отображение элементов Image, Clip и кнопки Submit (Edit.jsp)

<tr><td>Price</td> <td><input name="price" value="<%= product.getPrice() %>" /> </td> <td>Discount</td> <td><input name="discount" value="<%= Util.notNull(product.getDiscount()) %>" /></td> </tr> <tr><td>Quantity in Stock</td> <td><input name="quantity" value="<%= product.getQuantityInStock() %>" /></td> </tr> <tr><td>On Sale Date</td> <td colspan="3"><input name='onSaleDate' value='<%= product.getOnSaleDate() %>' /> (mm-dd-yyyy hh:mm:ss)</td> </tr> <tr><td>Description</td> <td colspan="3"> <textarea rows="5" cols="40" name="description"><%= Util.notNull(product.getDescription()) %> </textarea> </td> </tr> </table>

В нижней части этой JSP-страницы определено несколько вспомогательных методов. Первый из них называется output Image. Он призван отобразить элемент формы для объекта Image в нужных местах, которых может быть несколько. Этот метод не входит ни в один стандартный класс Java, вызываемый из JSP-страницы, так как он сильно связан с логикой представления. Полезно иметь отдельный класс для определения методов, которые выполняют в JSP-странице какие- либо задачи, не связанные с логикой представления; но в данном случае метод предназначен для получения кода HTML. Если вы поместите этот метод в отдельный класс, получится, что одна и та же страница обрабатывается в двух местах, в результате усложнится обслуживание кода. Лучше весь код, отвечающий за представление страницы, держать в одном месте.



Методу outputlmage, приведенному в листинге 6.56, передаются два параметра: строка, служащая идентификатором изображения, и сам объект Image. Имя поля input будет составлено из строки image (изображение) и переданного идентификатора. Таким образом вы получаете уникальное название для каждого изображения. Это название используется как для отображения на странице для пользователя, так и в качестве значения атрибута name объекта input. Каждый из атрибутов и элементов объекта Image отображается в отдельном текстовом поле, которое пользователь может заполнять.

Листинг 6.56. Вывод объекта Image (Edit.jsp)

<%! private String outputImage(String i, Image image) { String s; s = "<tr><td>" + i + ")</td>"; s += "<td>Format</td>"; s += "<td><input name='img" + i + "-format' value='" + Util.notNull(image.getFormat()) + "' /></td>" ; s += "<td></td><td>Source File</td>"; s += "<td><input name='img" + i + "-src' value='" + Util.notNull(image.getSrc()) + "' /></td>" ; s += "</tr>"; s += "<tr>"; s += "<td></td><td>Height</td>"; s += "<td><input name='img" + i + "-height' value='" + Util.notNull(image.getHeight()) + "' /></td>"; s += "<td></td><td>Width</td>"; s += "<td><input name='img" + i + "-width' value='" + Util.notNull(image.getWidth()) + "' /></td>"; s += "</tr>"; s += "<tr>"; s += "<td></td><td>Caption</td>"; s += "<td colspan='4'><textarea rows='5' cols='40' name='img" + i + "-caption'>" + Util.notNull(image.getCaption()) + " </textarea></td>"; s += "</tr>\n"; return s; } %>



Вывод объектов Author, Artist и Clip очень похож на вывод объектов Image. Каждый из элементов и атрибутов отображается в отдельной строке таблицы. Строка, которая идентифицирует конкретный объект, также используется двояким образом: для отображения в поле ввода и как значение атрибута name объекта input. Этот метод для объекта Clip показан в листинге 6.57.

Листинг 6.57. Отображение объекта Clip (Edit.jsp)

<%! private String outputClip( String i, Clip clip ) { String s; s = "<tr><td>" + i + ")</td>"; s += "<td>Format</td>"; s += "<td><input name='clip" + i + "-format' value='" + Util.notNull(clip.getFormat()) + "' /></td>" ; s += "<td></td><td>Source File</td>"; s += "<td><input name='clip" + i + "-src' value='" + Util.notNull(clip.getSrc()) + "' /></td>" ; s += "</tr>"; s += "<tr>"; s += "<td></td><td>Title</td>"; s += "<td colspan='3'><input name='clip" + i + "-title' value='" + Util.notNull(clip.getTitle()) + "' /></td>"; s += "</tr>"; s += "<tr>"; s += "<td></td><td>Length</td>"; s += "<td><input name='clip" + i + "-length' value='" + Util.notNull(clip.getLength()) + "' /></td>" ; s += "<td></td><td>Size</td>"; s += "<td><input name='clip" + i + "-size' value='" + Util.notNull(clip.getSize()) + "' /></td>"; s += "</tr>"; s += "<tr>"; s += "<td></td><td>Description</td>"; s += "<td colspan='4'><textarea rows='5' cols='40' name='clip" + i + "-description'>" + Util.notNull(clip.getDescription()) + "</textarea></td>" ; s += "</tr>"; return s; }

private String outputAuthor( String i, String author ) { String s = "<tr><td>Author</td>"; s += "<td><input name='author" + i + "' value='" + author + "' /> </td></tr>"; return s; }

private String outputArtist( String i, String artist ) { String s = "<tr><td>Artist</td>"; s += "<td><input name='artist" + i + "' value='" + artist + "' /> </td></tr>"; return s; } %>

 


Класс Catalog


Класс Catalog представляет корневой элемент DTD каталога, приведенного ниже: <!ELEMENT catalog (product_line*)>

У этого элемента нет атрибутов и имеется только один многократно повторяемый дочерний элемент с именем productjline. В листинге 6.1 приведена первая часть кода для объявления класса Catalog.

Листинг 6.1. Первая часть кода для объявления класса Catalog (Catalog.java)

package com.XmlEcomBook.Chap06;

import javax.xml.parsers.*;

import java.util.*;

import java.io.*;

import org.w3c.dom.*;

import org.xml.sax.*;

public class Catalog {

private Vector productLines = new Vector();

В этом коде формат имен, принятый в XML (нижний регистр с символом подчеркивания в качестве разделителя, например quantity_in_stock), был изменен на формат, соответствующий стандартному синтаксису Java. Имена классов начинаются с прописной буквы, и для разделения различных слов, составляющих одно имя, используются также прописные буквы, а не символы подчеркивания. Элемент с именем catalog становится классом Catalog.

Так как у элемента catalog может быть любое количество дочерних элементов product_l i ne, для их представления используется вектор Vector. Вообще, для представления повторяющихся элементов пригодна любая коллекция. Выбор конкретного вида коллекции основан на ее предполагаемом назначении; Vector является хорошим вариантом коллекции для тех случаев, когда вы не знаете заранее всех подробностей того, как будет использоваться коллекция.

Поскольку задачей данного класса является преобразование данных из XML к объектам Java и затем снова к XML, создается конструктор, которому в качестве параметра передается имя файла XML и который анализирует этот файл для извлечения из него данных. Конструктор показан в листинге 6.2.

Первая часть кода конструктора создает необходимые для анализа файла XML объекты JAXP. Также создается объект DocumentBuilder, который используется для анализа документа и получения объекта DOM Document.

В следующей части кода извлекается корневой элемент документа и все элементы типа product_l i ne. Выполняется цикл по этим элементам, и для каждого создается новый объект ProductLine; затем вызывается метод, который добавляет этот объект к объекту Catalog. Как вы видите, в классе Catalog требуется только найти прямые потомки объекта Catalog и передать их для дальнейшей обработки классу ProductLine.




Листинг 6.2. Конструктор класса Catalog (Catalog.java)

public Catalog( String filename) throws IOException { Document document = null; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.parse( new File( filename ) ); } catch( ParserConfigurationException pce ) { throw new IOException( "Parser Configuration Error" ); } catch( SAXException se ) { throw new IOException( "Parsing Excpetion" ); }

Element root = document.getDocumentElement(); NodeList nodes = root.getElementsByTagName ( "product_line" ); int num = nodes.getLength(); for( int i = 0; i < num; i++ ) { Element e = (Element)nodes.item( i ); ProductLine pl = new ProductLine( e ); addProductLine( pl ); } }

Устройство метода addProductLi ne, представленного в листинге 6.3, достаточно очевидно. Ему передается объект ProductLine, который затем добавляется в вектор productLines.



Листинг 6.З. Добавление новых серий товаров — элементов ProductLine (Catalog .Java)

public void addProductLine( ProductLine productLine ) { productLines.addEleraent( productLine ); }

Далее, вам нужен метод для доступа к объектам ProductLine, которые содержатся в объекте Catalog. Так как серии товаров идентифицируются по своим названиям, нужен метод, который извлекал бы объект на основании его имени. Этот метод показан в листинге 6.4. Методу getProductLine передается в качестве параметра название серии товаров, а затем в этом методе выполняется цикл по всем сериям товаров в каталоге, пока не будет найдена серия с этим именем. Если ни одна серия не найдена, возвращается nul 1.



Листинг 6.4. Поиск объекта ProductLine (Catalog.java)

public ProductLine getProductLine( String name ) { Enumeration enum = productLines.elements(); while( enum.hasMoreElements() ) { ProductLine pl = (ProductLine)enum.nextElement(); if( pl.getName().equals( name ) ) { return pl; } } return null; }

Другая операция, полезная при работе с каталогом, — получение конкретного объекта Product (товар) по его идентификатору. Эта операция позволяет отображать и редактировать отдельные элементы объекта Product. Для этого в каждом объекте, соответствующем серии товаров каталога, должен быть предусмотрен метод поиска конкретного товара. Детали реализации этого метода пока нас не интересуют, они будут рассматриваться в объекте ProductLine. Это типичный пример инкапсуляции, свойственной объектно-ориентированным языкам программирования. Листинг 6.5 иллюстрирует поиск товара по его идентификатору.





Листинг 6.5. Поиск товара в каталоге (Catalog.java)

public Product getProduct( String id ) { Enumeration enum = productLines.elements(); while( enum.hasMoreElements() ) { ProductLine pl = (ProductLine)enum.nextElement(); Product p = pl.getProduct( id ); if( p != null ) { return p; } } return null; }

Если не удалось обнаружить товар с указанным идентификатором, здесь также возвращается объект nul 1. Полезно выработать определенный принцип действия, если поиск оказался неудачным, и в таких случаях либо всегда возвращать null, либо всегда вызывать исключение. В программах, которые мы рассматриваем в этой главе, в случае неудачного поиска мы решили возвращать объект null, и всюду последовательно придерживаемся этого решения.

Объект Catalog позволяет осуществлять еще одно важное действие — удаление объектов Products. Этот метод, приведенный в листинге 6.6, аналогичен методу getProducts, но при нахождении указанного объекта он сначала удаляется из объекта ProductLine, а потом метод возвращает этот объект. Опять-таки, если объекта с указанным идентификатором не удалось обнаружить, возвращается объект null.



Листинг 6.6. Удаление товара из каталога (Catalog.java)

public Product deleteProduct( String id ) { Enumeration enum = productLines.elements(); while( enum.hasMoreElements() ) { ProductLine pl = (ProductLine)enum.nextElement(); Product p = pl.getProduct( id ); if( p != null ) { pl.deleteProduct( id ); return p; } } return null; }

Вам также необходимо, чтобы класс Catalog был способен выдавать содержащиеся в нем данные в виде документа XML. Для этого существует метод, представленный в листинге 6.7, который записывает данные с помощью объекта XMLWriter. Класс XMLWriter — служебный класс, который используется при выводе данных XML в выходной поток. Этот класс мы рассмотрим чуть позже, после класса Catal og. Как и в случае с конструктором класса Cal atog, где мы разобрали только начальную часть кода XML, которая необходима, чтобы можно было предоставить дальнейший анализ элементам ProductLine, здесь мы пишем только открывающий и закрывающий теги каталога. Запись элементов, соответствующих сериям товаров, является прерогативой самого класса ProductLine.



Листинг 6.7. Запись каталога в виде документа XML (Catalog.java)

public void toXML( XMLWriter writer ) throws IOException { writer.writeln( "<catalog>" ); writer.indent(); Enumeration enum = productLines.elements(); while( enum.hasMoreElements() ) { ProductLine pl = (ProductLine)enum.nextElement(); pl.toXML( writer ); writer.writeln( "" ); } writer.unindent(); writer.writeln( "</catalog>" ); } }

 




Класс Clip


Элемент clip очень похож на элемент image, что видно из его DTD:

<!ELEMENT clip (titie,description?)>

<!ATTLIST clip format (mp3|mpeg|mov|rm) #REQUIRED length CDATA #IMPLIED size CDATA #IMPLIED src CDATA #REQUIRED>

<!ELEMENT title (#PCDATA)>

Все атрибуты элемента clip имеют тип String, как показано в листинге 6.31. Причина этого заключается в том, что атрибуты length и size имеют свободный формат и допускают включение единиц измерения. Например, атрибут size (размер) может содержать строку типа "1.1 Mb".

Листинг 6.31. Начало кода класса Clip (Clip.java)

package com.XmlEcomBook.Chap06;

import org.w3c.dom.*; import java.io.*;

public class Clip extends Object {

private String format; private String length; private String size; private String src; private String title; private String description;

Одному из конструкторов класса Clip не передается никаких аргументов; этот конструктор создает стандартный объект Clip. Другой конструирует объект Clip на основе объекта DOM Element, как показано в листинге 6.32. В первых четырех строках второго конструктора извлекаются атрибуты элемента clip. Элемент description обрабатывается тем же способом, что элемент description в product и элемент caption в Image, то есть извлекается простой текст XML и хранится в виде строки. Последняя строка второго конструктора извлекает значение элемента ti tl e также в виде строки.

Листинг 6.32. Конструкторы класса Clip (Clip.java)

public Clip() { }

public Clip( Element clipElement ) { format = clipElement.getAttribute( "format" ); length = clipElement.getAttribute( "length" ); size = clipElement.getAttribute( "size" ); src = clipElement.getAttribute( "src" ); NodeList descList = clipElement.getElementsByTagName( "description" ); if( descList.getLength() > 0 ) { Element descElement = (Element)descList.item( 0 ); description = Util.extractMarkupAsText(descElement.getChildNodes()); } title = Util.extractTextFrom( "title", clipElement ); }


В классе Clip также имеются методы setXxx и getXxx для каждого из его шести полей. Они включены в прилагаемый к книге компакт-диск, но здесь не приводятся. Метод toXML аналогичен тем, которые мы изучали ранее для других классов. Как и у других объектов данных, у класса Clip имеется метод для преобразования объекта в код XML. Этот метод показан в листинге 6.33.



Листинг 6.33. Преобразование класса Clip в XML (Clip.java)

public void toXML( XMLWriter writer ) throws IOException { writer.write( "<clip " ); writer.write( "format='" + format + "' " ); if( length != null ) writer.write( "length='" + length + "' " ); if( size != null ) writer.write( "size='" + size + "' " ); writer.writeln( "src='" + src + "'>" ); writer.indent(); writer.writeln( "<title>" + title + "</title>" ); if( description != null ) writer.writeln( "<description>" + description + "</description>" ); writer.unindent(); writer.writeln( "</clip>" ); }

}






Класс DateTime


Класс DateTime используется для представления даты. Он основан на следующем фрагменте DTD:

<!ENTITY % date_time "(day_of_week?.month?,day_of_month?, year?,(hour,minute,seconds?)?)"> <!ELEMENT day_of_week (#PCDATA)> <!ELEMENT month (#PCDATA)> <!ELEMENT day_of_month (#PCOATA)> <!ELEMENT year (#PCDATA)> <!ELEMENT hour (#PCDATA)> <!ELEMENT minute (#PCDATA)> <!ELEMENT seconds (#PCDATA)>

Параметрическая сущность datejtime используется для представления даты и времени. В DTD каталога эта сущность задействуется только в элементе onsale_date, но, если расширить DTD, ее можно использовать и в других местах. В классе DateTime имеется свое поле для каждого из элементов, определенных в DTD. Также в нем имеются два конструктора, один из которых не имеет аргументов, а другому в качестве аргумента передается элемент, подлежащий анализу. Они показаны в листинге 6.34.

Листинг 6.34. Начало кода класса DateTime (DateTime.java)

package com.XmlEcomBook.Chap06;

import org.w3c.dom.Element; import java.util.StringTokenizer; import java.io.IOException; import java.io.OutputStream;

public class DateTime extends Object {

private String dayOfWeek = null; private Integer month = null; private Integer dayOfMonth = null; private Integer year = null; private Integer hour = null; private Integer minute = null; private Integer seconds = null;

public DateTime() { }

public DateTime(Element dateElement) { dayOfWeek = Util.extractTextFrom( "day_of_week", dateElement ); month = Util.extractIntFrom( "month", dateElement ); dayOfMonth = Util.extractIntFrom ( "day_of_month", dateElement ); year = Util.extractIntFrom( "year", dateElement ); hour = Util.extractIntFrom( "hour", dateElement ); minute = Util.extractIntFrom( "minute", dateElement ); seconds = Util.extractIntFrom( "seconds", dateElement ); }

Класс DateTime анализирует дату и время, полученные из форм HTML. Анализ осуществляется с помощью метода fromString, приведенного в листинге 6.36. Этот метод использует объект StringTokerrizer для разделения строки на фрагменты — лексемы. Каждая лексема, выделенная из строки объектом String- Tokenizer, проходит проверку. Если лексема содержит в себе строку day (день), то это день недели, и соответствующее значение сохраняется в поле dayOfWeek (день недели). Так как часть строки, содержащая дату, разделена символами дефиса (-), то при обнаружении такого символа из строки выделяются лексемы, идентифицирующие месяц, день месяца и год. Если в строке обнаружен символ двоеточия (:), то это указывает, что данная часть строки содержит значение времени суток, поэтому из строки выделяются часы, минуты и секунды.


Листинг 6.35. Метод fromString (DateTime.java)

public void fromString( String newDate ) { StringTokenizer tokenizer = new StringTokenizer( newDate, " " ); while( tokenizer.hasMoreTokens() ) { String next = tokenizer.nextToken(); if( next.indexOf( "day" ) > 0 ) { dayOfWeek = next; } if( next.indexOf( '-' ) > 0 ) { int first = next.indexOf( '-' ); int second = next.indexOf( '-', first + 1 ); month = new Integer( next.substring( 0, first ) ); dayOfMonth = new Integer( next.substring( first + 1, second ) ); year = new Integer( next.substring( second + 1, next.length() ) ); } if( next.indexOf( ':' ) > 0 ) { int first = next.indexOf( ':' ); int second = next.indexOf( ':', first + 1 ); hour = new Integer( next.substring( 0, first ) ); minute = new Integer( next.substring( first + 1, second ) ); seconds = new Integer( next.substring( second + 1, next.length() ) ); } } }

Последний метод этого класса, toXML, показан в листинге 6.36. Он аналогичен другим методам toXML, и записывает по отдельности значение каждого поля, снабжая его соответствующими тегами.

Листинг 6.36. Метод toXML (DateTime.java)

public void toXML( XMLWriter writer ) throws IOException { writer.writeln( "<onsale_date>" ); writer.indent(); if( dayOfWeek != null ) writer.writeln( "<day_of_week>" + dayOfWeek + "</day_of_week>" ); if( month != null ) writer.writeln( "<month>" + month + "</month>" ); if( dayOfMonth != null ) writer.writeln( "<day_of_month>" + dayOfMonth + "</day_of_month>" ); if( year != null ) writer.writeln( "<year>" + year + "</year>" ); if( hour != null ) writer.writeln( "<hour>" + hour + "</hour>" ); if( minute != null ) writer.writeln( "<minute>" + minute + "</minute>" ); if( seconds != null ) writer.writeln( "<seconds>" + seconds + "</seconds>" ); writer.unindent(); writer.writeln( "</onsale_date>" ); } }

 


Класс Image


Очередной класс, который мы рассмотрим, — это класс Image. Он основан на следующем определении DTD:

<!ELEMENT image (caption?)>

<!ATTLIST image format (gif|png|jpg) #REQUIRED width CDATA #IMPLIED height CDATA #IMPLIED src CDATA

#REQUIRED>

<!ELEMENT caption (paragraph)* >

Определенный здесь элемент caption аналогичен элементу description в объекте product. Поскольку структура элемента paragraph достаточно сложная, мы представим этот элемент в виде простой строки XML, которую пользователь может непосредственно редактировать. Атрибуты format и scr можно представить в виде полей типа String. Атрибуты height и width не являются обязательными, поэтому следует учесть, что иногда эти атрибуты будут отсутствовать. Для этого нужно использовать класс Integer, в котором имеется значение null, означающее отсутствие атрибута. Начало класса Image показано в листинге 6.28.

Листинг 6.28. Начало кода класса Image (Image.java)

package com.XmlEcomBook.Chap06;

import java.io.*; import org.w3c.dom.*;

public class Image {

private String format; private Integer width; private Integer height; private String src; private String caption;

Конструкторы класса Image действуют по тому же принципу, что и конструкторы других классов связывания данных. Конструктор без аргументов создает объект Image, устанавливаемый по умолчанию; затем второй конструктор создает объект Image на основе объекта DOM Element. Аргументы scr и format извлекаются непосредственно из элемента image. Для атрибутов width и height используется служебный метод get Integer, так как существует вероятность того, что эти атрибуты не будут присутствовать, поскольку они являются необязательными. Метод getlnteger возвращает null в тех случаях, когда ему передается пустой объект. Наконец, если существует элемент caption, его содержимое извлекается как текст. Эти конструкторы показаны в листинге 6.29.

Листинг 6.29. Конструкторы класса Image (Image.java)

public Image( Element imgElement) { format = imgElement.getAttribute( "format" ); width = Util.getInteger( imgElement.getAttribute( "width" ) ); height = Util.getInteger( imgElement.getAttribute( "height" ) ); src = imgElement.getAttribute( "src" ); NodeList captionList = imgElement.getElementsByTagName( "caption" ); if( captionList.getLength() > 0 ) { Element captionElement = (Element)captionList.item( 0 ); caption = Util.extractMarkupAsText(captionElement.getChildNodes()); } }


Каждое из полей класса Image имеет методы getXxx и setXxx. Эти простые методы задают или возвращают значение соответствующего поля. Из соображений экономии места мы не приводим их в книге, но на прилагаемом компакт-диске все эти методы присутствуют.

Метод toXML, который генерирует элемент image, показан в листинге 6.30. Открывающий тег содержит четыре атрибута — атрибуты format и scr присутствуют всегда, так как они являются обязательными, а для необязательных атрибутов width и height организована проверка их наличия. Перед тем как записать их, следует проверить, содержат ли они какое-либо значение (отличное от null).



Листинг 6.30. Преобразование Image в XML (Image.java)

public void toXML( XMLWriter writer ) throws IOException { writer.write( "<image " ); writer.write( "format='" + format + "' " ); if( width != null ) writer.write( "width='" + width + "' " ); if( height != null ) writer.write( "height='" + height + "' " ); writer.writeln( "src='" + src + "'>" ); writer.indent(); if( caption != null ) writer.writeln( "<caption>" + caption + "</caption>" ); writer.unindent(); writer.writeln( "</image>" ); } }

 




Класс Main сервлета


Класс Main сервлета, в котором обрабатывается информация, введенная пользователем на главной HTML-странице (main.html), показан в листинге 6.45. Центральной точкой входа в этот класс сервлета является метод doGet. После задания исходных значений некоторых переменных этот метод пытается получить объект Session из запроса. Если этого объекта еще не существует, нужно создать новый объект Session. Затем создается новый объект Catalog на основе файла catalog.xml. Этот объект Catalog присоединяется к сеансу с помощью метода setAttribute. Если же обнаружен существующий объект Session, объект catalog можно получить как атрибут уже существующего сеанса.

Листинг 6.45. Начало кода класса Main и метод doGet (Main.java)

import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import com.XmlEcomBook.Chap06.*;

public class Main extends HttpServlet { static private final String FILE_NAME = "catalog.xml";

public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { // Access the output stream. PrintWriter out = res.getWriter(); res.setContentType("text/html"); HttpSession session = req.getSession(false); Catalog catalog = null;

if( session == null ) { //There is no session, create a new one session = req.getSession( true ); catalog = new Catalog( FILE_NAME ); session.setAttribute( "catalog", catalog ); } else { catalog = (Catalog)session.getAttribute( "catalog" ); }

После этого метод doGet переходит к выполнению следующей задачи — извлечению всех значений, которые пользователь ввел в форму HTML, как показано в верхней части листинга 6.46. Эти значения извлекаются из объекта HttpServl etRequest с помощью метода getParameter. Затем эти значения проверяются на наличие ошибок. В первую очередь, если выбрана одна из основных операций (добавление, удаление или редактирование товара), проверяется, указан ли идентификатор товара. Затем из каталога извлекается элемент (товар) с этим идентификатором. Если выполняется операция по удалению или редактированию элемента, требуется проверить, что этот элемент действительно присутствует в каталоге, иначе выдается сообщение об ошибке. Если выполняется операция по добавлению товара в каталог, то нужно проверить, нет ли уже в каталоге товара с таким идентификатором, поскольку каждый идентификатор должен быть уникален. Также при добавлении товара в каталог нужно убедиться, что указанная для товара серия действительно присутствует в каталоге. Нужно проверить состояние булевой переменной error (признак ошибки); если ее значение установлено равным true, то нужно отобразить страницу с сообщением об ошибке. Метод, который отображает эту страницу, будет рассмотрен далее в этом разделе.


Листинг 6.46. Проверка ошибок во введенных параметрах (Main.java)

String operation = req.getParameter("operation"); String productID = req.getParameter("productid"); String productLine = req.getParameter("productline");

boolean error = false; String errorMsg = "Unkown Error"; if( operation.equals( "delete" ) || operation.equals( "edit" ) || operation.equals( "add" ) ) { if( productID == null || productID.equals("") ) { error = true; errorMsg = "You must select a product ID with the " + operation + " operation."; } else { Product product = catalog.getProduct( productID ); if( operation.equals( "edit" ) || operation.equals( "delete" ) ) { if( product == null ) { error = true; errorMsg = "Invalid product ID:" + productID; } } if( operation.equals( "add" ) ) { if( product != null ) { error = true; errorMsg = "Cannot add, product id " + productID + " already exists."; } ProductLine pl = catalog.getProductLine( productLine ); if( pl == null ) { error = true; errorMsg = "No product line " + productLine + " exists."; } } } }

if( error ) { outputPage( out, "Error", errorMsg ); return; }

После того как вы убедились, что все параметры введены правильно, можно приступить к их фактической обработке, как показано в листинге 6.47. У вас имеется последовательность инструкций if для определения, какие действия следует выполнять в том или ином случае. Для добавления и редактирования данных о товаре используется одна и та же JSP-страница. Чтобы вызвать из сервлета JSP- страницу, вызывается метод getRequestDi spatcher объекта класса ServletContext и задается URL-адрес относительно корневой папки сервлета (например, Edit.jsp). После получения объекта класса RequestDi spatcher вызывается его метод forward, которому в качестве параметров передаются объекты классов HttpServletRequest и HttpServl etResponse, то есть текущие запрос (req) и ответ (res). Это позволяет передать JSP-странице все текущие параметры. Операция по удалению товара аналогичным образом вызывает другую JSP-страницу. Операция по отмене всех изменений выполняется непосредственно в методе doGet. Для этого просто вызывается метод invalidate объекта session. Этот метод делает текущий сеанс недействительным и прекращает все связи с объектами. Последняя команда, которая сохраняет все сделанные в течение сеанса изменения, также выполняется непосредственно в методе doGet. Здесь создается новый объект класса XMLWriter, использующий файл catalog.xml. Туда вписываются необходимые строки, содержащие объявление XML и объявление типа документа. Затем используется метод toXML объекта catal og для того, чтобы записать данные в формате XML в этот файл.



Листинг 6.47. Обработка различных операций (Main.java)

if( operation.equals( "add" ) || operation.equals( "edit" ) ) { getServletContext().getRequestDispatcher ("/Edit.jsp").forward(req, res); } if( operation.equals( "delete" ) ) { getServletContext(). getRequestDispatcher("/Delete.jsp").forward(req, res); } if( operation.equals( "refresh" ) ) { session.invalidate(); outputPage( out, "Session Cancelled", " The session has been canelled" ); return; } if( operation.equals( "save" ) ) { try { FileOutputStream outFile = new FileOutputStream( FILE_NAME ); XMLWriter writer = new XMLWriter( outFile ); writer.writeln( "<?xml version='1.0' standalone='no' ?>" ); writer.writeln( "<!DOCTYPE catalog SYSTEM 'catalog.dtd'>" ); catalog.toXML( writer ); } catch( IOException e ) { outputPage( out, "Error", "I/O Exception writing XML file" ); return; } outputPage( out, "Changes saved", "The changes have been saved" ); } }

В сервлете Main имеется метод для создания и отображения простой HTML- страницы. Этот метод приведен в листинге 6.48. Ему передается объект PrintWriter, заголовок и текст, который будет отображен на странице. На этой странице также будет расположена ссылка на главную HTML-страницу.

Листинг 6.48. Отображение страницы (Main.java)

private void outputPage( PrintWriter out, String title, String text ) { out.println("<html><head><title>" + title ); out.println("</title></head><body>"); out.println("<p>" + text + "</p>"); out.println("<a href='/main.html'>Return to main page.</a>"); out.println("</body></html>"); } }

 


Класс Product


Элемент product сложнее, чем элементы catalog и product_line, которые мы уже рассматривали. Ниже приводится DTD для этого элемента:

<!ELEMENT product (name.author*,artist*.description,price, quantity_in_stock,image*,onsale_date?,clip*)> <!ATTLIST product id ID #REQUIRED> <!ATTLIST product keywords CDATA #IMPLIED>

Элемент product может содержать до девяти различных типов дочерних элементов, многие из которых могут повторяться либо, напротив, отсутствовать. Этот элемент имеет два атрибута. В листинге 6.17 определены поля для каждого из этих элементов и атрибутов, во многом аналогично тому, как это было сделано для предыдущих классов. По-прежнему для повторяющихся элементов используется вектор Vector. Большая часть других элементов имеет тип Stri ng, но здесь есть несколько исключений. Так как мы знаем, что элемент price определяет цену, то есть доллары и центы, то для его представления используется тип double. Элемент quantity_in_stock содержит целое число, поэтому для его представления требуется тип int. Элемент on_sale_date представлен классом Date.

Листинг 6.17. Начало кода класса Product (Product.java)

package com.XmlEcomBook.Chap06;

import java.io.*; import java.util.*; import java.text.*; import org.w3c.dom.*;

public class Product {

private String id; private String keywords; private String name; private Vector authors = new Vector(); private Vector artists = new Vector(); private String description; private double price; private Integer discount; private int quantityInStock; private Vector images = new Vector(); private DateTime onSaleDate = new DateTime(); private Vector clips = new Vector();

Посмотрев на определения полей, вы можете заметить, что одно из них вам незнакомо. Для поля discount не имеется соответствующего дочернего элемента или атрибута в элементе product. Так получилось потому, что элемент price довольно-таки просто устроен — он содержит данные типа PCDATA и один атрибут discount, как показано ниже:

<!ELEMENT price (#PCDATA)>


<!ATTLIST price discount CDATA #IMPLIED>

Вместо того чтобы создавать для этого элемента новый класс всего лишь с двумя полями, мы включим оба этих поля прямо в класс Product. Поле discount имеет тип Integer. В данном случае мы не можем задействовать примитивный тип int, так как элемент discount является необязательным и может отсутствовать. В такой ситуации нужно воспользоваться объектом null типа Integer.

Другое отличие связано с полем description. В DTD это поле было представлено следующим образом:

<!ENTITY % running_text "(#PCDATA|bold|italics|

quote|link|general)*">

<!ELEMENT description (paragraph|general)* >

<!ELEMENT paragraph %running_text;>

<!ELEMENT bold (#PCDATA)>

<!ELEMENT italics (#PCDATA)>

<! ELEMENT quote (#PCDATA)>

<!ATTLIST quote attrib CDATA #IMPLIED>

<!ELEMENT link (#PCDATA)>

<!ATTLIST link href CDATA «REQUIRED

alt CDATA #IMPLIED>

<!ELEMENT general (#PCDATA)>

<!ATTLIST general type CDATA #REQUIRED>

Это сложная модель содержимого, включающая большое количество дочерних элементов, которые могут повторяться в произвольном порядке, что приводит к следующим двум затруднениям. Первое связано с представлением поля description в Java-программе. Вам нужно было бы создать класс description, которому можно передать объекты paragraph и general, а затем этот класс снова должен был бы получать к ним доступ. Сам класс Paragraph был бы классом сложного типа, способным содержать любой из своих пяти дочерних элементов и текстовых элементов. Второе затруднение связано с отображением всех этих данных в виде, допускающем редактирование. Было бы очень трудно отобразить эту структуру в формате HTML. Мы решили упростить обработку этих элементов.

Если вы посмотрите на определения полей в классе Product, вы увидите, что тип description определен как String. Вместо того чтобы копировать структуру элемента description, мы предпочли просто хранить ее в виде необработанной строки XML, то есть в виде простого текста, содержащего символьные данные вместе с разметкой. Когда нам потребуется предоставить пользователю описание товара (содержимое элемента description), мы просто отобразим необработанный текст XML, который и будет редактироваться пользователем.



Для класса Product предусмотрены два конструктора, показанные в листинге 6.18. Первый из них не имеет никаких аргументов и используется для создания нового элемента Product. Другому конструктору передается объект DOM Element, и на основе исходного кода XML конструктор создает объект Product. Конструктор сначала получает два атрибута, id и keywords, из элемента product. Для остальных полей созданы методы для извлечения всех элементов, дочерних по отношению к элементу product.

Листинг 6.18. Конструкторы класса Product (Product.java)

public Product() { }

public Product( Element productElement ) { id = productElement.getAttribute( "id" ); keywords = productElement.getAttribute( "keywords" ); extractName( productElement ); extractAuthors( productElement ); extractArtists( productElement ); extractDescription( productElement ); extractPrice( productElement ); extractQuantityInStock( productElement ); extractImages( productElement ); extractDate( productElement ); extractClips( productElement ); }

Метод extractName устроен достаточно просто. Он показан в листинге 6.19. В нем используется служебный метод extractTextFrom, который извлекает текст из элемента name. В методах extractAuthors и extractArti sts также используется этот служебный метод. Оба этих метода получают список элементов, дочерних по отношению к элементу product, а затем осуществляют цикл по этим элементам, извлекая из каждого содержащийся в нем текст и вызывая соответствующий метод для добавления элемента к объекту.



Листинг 6.19. Методы extractName, extractAuthors и extractArtists (Product.java)

private void extractName( Element productElement ) { name = Util.extractTextFrom( "name", productElement ); }

private void extractAuthors( Element productElement ) { NodeList authorList = productElement.getElementsByTagName( "author" ); for( int i = 0; i < authorList.getLength(); i++ ) { Element author = (Element)authorList.item(i); addAuthor( Util.extractTextFrom( "name", author ) ); } }



private void extractArtists( Element productElement ) { NodeList authorList = productElement.getElementsByTagName( "artist" ); for( int i = 0; i < authorList.getLength(); i++ ) { Element author = (Element)authorList.item(i); addArtist( Util.extractTextFrom( "name", author ) ); } }

Теперь мы перейдем к рассмотрению метода extractDescription. Он приведен в листинге 6.20. Этот метод получает элемент description из элемента product. Поскольку в интерфейсе API модели DOM не предусмотрена возможность получения только одного элемента с определенным именем, требуется метод getEle- mentsByTagName, который возвращает коллекцию узлов. Затем нужно проверить длину этой коллекции. Если эта длина больше нуля, в ней имеется элемент description. В таком случае нужно извлечь первый подходящий элемент, поскольку элемент description в коллекции только один. Наконец, мы используем служебный метод extractMarkupAsText для того, чтобы получить содержимое элемента description.



Листинг 6.20. Метод extractDescription (Product.java)

private void extractDescription( Element productElement ) { NodeList desc = productElement.getElementsByTagName( "description" ); if( desc.getLength() > 0 ) { NodeList contents = desc.item(0).getChildNodes(); description = Util.extractMarkupAsText( contents ); } }

Метод extractPrice должен извлечь содержимое элемента price, что определит фактическую цену, а также значение атрибута discount для этого элемента. В первой строке этого метода извлекается текстовое содержимое элемента price с помощью служебного метода. Полученный текст преобразуется к типу double с помощью объекта NumberFormat. Этот объект способен проанализировать полученный текст и извлечь из строки, содержащей дополнительные символы (в частности, знак $), само число, представляющее цену. Затем нам нужно получить информацию о скидках. Для этого извлекается коллекция элемента price, а из нее — атрибут discount. Затем на основе строки (значения атрибута discount) создается объект типа Integer. Этот метод показан в листинге 6.21.





Листинг 6.21. Метод extractPrice (Product.java)

private void extractPrice( Element productElement ) { String s = Util.extractTextFrom( "price", productElement ); NumberFormat nf = NumberFormat.getCurrencyInstance(); try { Number n = nf.parse( s ); price = n.doubleValue(); } catch( ParseException pe ) { price = 0; } NodeList priceNodes = productElement.getElementsByTagName( "price" ); if( priceNodes.getLength() > 0 ) { Element price = (Element)priceNodes.item(0); if( price != null ){ String d = price.getAttribute( "discount" ); if( d != null && !d.equals( "" ) ) { discount = new Integer( d ); } } } }

Извлечь информацию из элемента quantity_in_stock сравнительно легко. Используется знакомый нам метод extractTextFrom для получения строки, которая затем анализируется и преобразуется к типу i nt, как показано в листинге 6.22.



Листинг 6.22. Метод extractQuantitylnStock (Product.java)

private void extractQuantityInStock( Element productElement ) { String s = Util.extractTextFrom ( "quantity_in_stock", productElement ); quantityInStock = Integer.parseInt( s ); }

Элемент image может встречаться несколько раз, поэтому нам необходимо извлечь все дочерние элементы product с именем image, а затем использовать метод addlmage для добавления их к объекту product. Метод extractClips практически идентичен методу extractlmages, только он извлекает не изображения, а клипы. Оба эти метода приведены в листинге 6.23.

Листинг 6.23. Методы extractlmages и extractClips (Product.java)

private void extractImages( Element productElement ) { NodeList descNode = productElement.getElementsByTagName( "image" ); for( int i = 0; i < descNode.getLength(); i++ ) { addImage( new Image( (Element)descNode.item(i) ) ); } }

private void extractClips( Element productElement ) { NodeList descNode = productElement.getElementsByTagName( "clip" ); for( int i = 0; i < descNode.getLength(); i++ ) { addClip( new Clip( (Element)descNode.item(i) ) ); } }



Последний метод, который мы рассмотрим в связи с извлечением данных, — это метод extractDate, приведенный в листинге 6.24. В нем используется метод DOM getElementsByTagName для извлечения коллекции узлов с именем onsale_date. В случае обнаружения такого узла именно он используется для создания нового объекта Date (поскольку существует один и только один элемент onsale_date).



Листинг 6.24. Метод extractDate (Product.java)

private void extractDate( Element productElement ) { NodeList date = productElement.getElementsByTagName( "onsale_date" ); if( date.getLength() > 0 ) { onSaleDate = new DateTime( (Element)date.item( 0 ) ); } }

Теперь нам нужны методы для извлечения и задания значений всех полей. Большинство из этих методов достаточно просты, они похожи на методы getName и setName из листинга 6.25. Для полей id, keywords, description, price, discount, quantitylnStock и onSaleDate существуют аналогичные методы. Мы не приводим их здесь из соображений экономии места, но все они включены в исходный код примеров, находящихся на прилагаемом к книге компакт-диске.



Листинг 6.25. Методы getName и setName (Product.java)

public String getName() { return name; }

public void setName( String newName ) { name = newName; }

Поле authors относится к типу Vector, поэтому его нужно обрабатывать иными методами, чем приведенные выше методы getXxx и setXxx. У нас имеется метод addAuthor, который добавляет строку к вектору authors с помощью метода addElement. Чтобы удалить авторов из списка, имеется метод removeAll Authors, который удаляет всех авторов из элемента Product. Таким образом, можно заменять список авторов, для этого требуется удалить весь предыдущий список и добавить новый. Имеется также метод getAuthors, который возвращает перечень всех авторов. Для удаления, добавления и получения элементов artist, image и clip используются такие же методы, как и для элементов author, только все операции выполняются над соответствующими векторами. Эти методы показаны в листинге 6.26.





Листинг 6.26. Операции над элементами Author и Artist (Product.java)

public void addAuthor( String newAuthor ) { authors.addElement( newAuthor ); }

public void removeAllAuthors() { authors.removeAllElements(); }

public Enumeration getAuthors() { return authors.elements(); }

Как и другие объекты связывания данных, объекты Product преобразуются в формат XML с помощью метода toXML, приведенного в листинге 6.27. Этот метод начинается с записи открывающего тега для элемента product. Атрибут id является обязательным, поэтому его нужно записывать всегда. Поскольку элемент keywords является необязательным, всегда нужно проверять его наличие, прежде чем записывать. Затем мы записываем дочерние элементы, поэтому нужно сделать отступ. Дочерний элемент name не вызывает никаких затруднений, так как это просто строка, то есть объект типа String. Вам нужно просто записать открывающий тег, саму строку и закрывающий тег. Элементы author и artist являются повторяющимися, поэтому нужно выполнить цикл по вектору Vector и записать каждый элемент по отдельности. Элементы description, price и quantity_in_stock представляют собой простые строки, так что они записываются тем же способом, что и элемент name. Вектор изображений содержит экземпляры класса Image, поэтому нужно сделать цикл по этому вектору и к каждому изображению применить метод toXML К объекту onSaleDate также применяется метод toXML, а вектор, содержащий элементы clip, обрабатывается так же, как и другие вектора, то есть с помощью цикла. К каждому из составляющих вектор объектов clip поочередно применяется метод toXML. Наконец, нужно установить отступ равным исходному и написать закрывающий тег элемента product.



Листинг 6.27. Преобразование Product в XML (Product.java)

public void toXML( XMLWriter writer ) throws IOException { writer.write( "<product id='" + id + "'" ); if( keywords != null ) writer.write( " keywords='" + keywords + "'" ); writer.writeln( ">" ); writer.indent(); writer.writeln( "<name>" + name + "</name>" ); for( int i = 0; i < authors.size(); i++ ) { writer.writeln( "<author>" + authors.elementAt( i ) + "</author>" ); } for( int i = 0; i < artists.size(); i++ ) { writer.writeln( "<artist>" + artists.elementAt( i ) + "</artist>" ); } writer.writeln( "<description>" + description + "</description>" ); writer.writeln( "<price>" + price + "</price>" ); writer.writeln( "<quantity_in_stock>" + quantityInStock + "</quantity_in_stock>" ); for( int i = 0; i < images.size(); i++ ) { Image image = (Image)images.elementAt( i ); image.toXML( writer ); } if( onSaleDate != null ) onSaleDate.toXML( writer ); for( int i = 0; i < clips.size(); i++ ) { Clip clip = (Clip)clips.elementAt( i ); clip.toXML( writer ); } writer.unindent(); writer.writeln( "</product>" ); }

}






Класс ProductLine


Следующий класс, ProductLine, основан на определении DTD для элемента productline:

<!ELEMENT product_line (product*) > <!ATTLIST product_line name CDATA #IMPLIED>

Как и в элементе catalog, в этом элементе содержится один повторяющийся дочерний элемент. Поэтому и здесь мы применяем для представления этих дочерних элементов вектор Vector, как показано в листинге 6.12. У этого элемента имеется атрибут name, для которого тоже необходимо создать представление в классе. Добавим поле, имеющее тип Srting. Тип String обычно хорошо подходит для элементов CDATA.

Листинг 6.12. Начало кода класса ProductLine (ProductLine.java)

package com.XmlEcomBook.Chap06;

import java.util.*; import java.io.*;

import org.w3c.dom.*;

public class ProductLine extends Object {

private String name; private Vector products = new Vector();

Для этого класса у нас имеются два конструктора. Первый конструктор не имеет аргументов и просто создает пустой объект ProductLine для представления

серии товаров без указания имени и товаров. Второй конструктор этого класса использует объект DOM Element. Вы уже видели, что Element передается конструктору классом Catalog. Этот конструктор должен быть способен получить атрибут name этого элемента, а затем найти все дочерние элементы, соответствующие товарам данной серии, и создать новые элементы Product на их основе. Эти конструкторы показаны в листинге 6.13.

Листинг 6.13. Конструкторы ProductLine (ProductLine.java)

public ProductLine() { }

public ProductLine( Element element ) { name = element.getAttribute( "name" ); NodeList productNodes = element.getElementsByTagName( "product" ); int num = productNodes.getLength(); for( int i = 0; i < num; i++ ) { addProduct( new Product( (Element)productNodes.item( i ) ) ); } }

Теперь перейдем к методам, открывающим доступ к элементам Product и позволяющим их модифицировать. Они аналогичны методам элемента Catalog, которые использовались для вектора, состоящего из элементов ProductLine. Один метод, показанный в листинге 6.14, требуется для получения элементов (товаров) по их имени, другой — для удаления элементов и еще один — для их добавления в каталог.




Листинг 6.14. Методы для получения и модификации элементов Product (ProductLine.java)

public void addProduct( Product product ) { products.addElement( product ); }

public Product deleteProduct( String id ) { Enumeration enum = products.elements(); while( enum.hasMoreElements() ) { Product p = (Product)enum.nextElement(); if( p.getId().equals( id ) ) { products.remove( p ); return p; } } return null; }

public Product getProduct( String id ) { Enumeration enum = products.elements(); while( enum.hasMoreElements() ) { Product p = (Product)enum.nextElement(); if( p.getId().equals( id ) ) { return p; } } return null; }

Также необходимо иметь возможность получать доступ к полю name элемента Product и модифицировать это поле, как показано в листинге 6.15.



Листинг 6.15. Методы для получения и модификации имени элементов Product (ProductLine.java)

public String getName(){ return name; }

public void setName( String newName ) { name = newName; }

Наконец, нам нужен метод для записи данных в формате XML. Этот метод приведен в листинге 6.16. Как и в классе Catalog, здесь имеется метод с названием toXML, который записывает код XML в указанный объект класса XMLWriter. В этот раз нам также нужно записать атрибут элемента name. В XML разрешается помещать значения атрибутов либо в одинарные, либо в двойные кавычки. То есть вы можете написать name = "ABC" или name = 'ABC' — обе записи эквивалентны. Чтобы упростить вывод данных в формате XML, следует пользоваться одинарными кавычками, так как при введении двойных кавычек их придется снабжать обратной косой чертой, что затрудняет восприятие кода.



Листинг 6.16. Запись данных ProductLine в формате XML (ProductLine.java)

public void toXML( XMLWriter writer ) throws IOException { writer.write( "<product_line" ); if( name != null ) writer.write( " name='" + name ); writer.writeln( "'>" ); writer.indent(); Enumeration enum = products.elements(); while( enum.hasMoreElements() ) { Product p = (Product)enum.nextElement(); p.toXML( writer ); writer.writeln( "" ); } writer.unindent(); writer.writeln( "</product_line>" ); }

}

 




Класс Util


В классе Util имеется несколько удобных служебных методов, которые вызываются как из объекта данных, так и из классов представления. Все они являются открытыми и статическими, поэтому их можно вызывать откуда угодно. Первые два метода этого класса, приведенные в листинге 6.37, используются для извлечения информации из данных в формате XML. Метод extractTextFrom ищет конкретный элемент, и если находит, извлекает его текстовое содержимое и возвращает. Следующий метод, extractlntFrom, использует первый метод для извлечения текста из элемента, а затем преобразует этот текст в формат Integer, после чего возвращает полученное значение.

Листинг 6.37. Начало кода класса Util и методы extractTextFrom и extractlntFrom (Util.java)

package com.XmlEcomBook.Chap06;

import org.w3c.dom.*; import java.io.*;

public class Util {

static public String extractTextFrom ( String childElementName, Element element ) { NodeList nameList = element.getElementsByTagName ( childElementName ); if( nameList.getLength() < 1 ) { return null; } Text nameText = (Text)nameList.item(0).getFirstChild(); return nameText.getData(); }

static public Integer extractIntFrom ( String childElementName, Element element ) { String s = Util.extractTextFrom( childElementName, element ); if( s == null || s.equals( "" ) ) { return null; } return new Integer( s ); }

Следующий метод также помогает работать с данными XML. Этому методу, приведенному в листинге 6.38, передается объект NodeList (список узлов), а возвращается он уже в виде текста. Основная обработка происходит в цикле for, где конструируется строка. Для каждого узла из списка узлов к строке добавляется символ открывающей угловой скобки (<) и имя тега. Затем в открывающий тег добавляется каждый из атрибутов этого узла, и тег завершается символом закрывающей угловой скобки (>). После этого рекурсивным образом вызывается метод extractMarkupAsText, чтобы преобразовать все дочерние элементы текущего узла к текстовому формату. Если узел, обрабатываемый в цикле for, является текстовым, то он непосредственно добавляется к строке.


Листинг 6.38. Метод extractMarkupAsText (Util.java)

static public String extractMarkupAsText( NodeList nodeList ) { //recursively extract String text = ""; if( nodeList != null ) { for( int i = 0; i < nodeList.getLength(); i++ ) { Node node = nodeList.item(i); if( node instanceof Element ) { Element el = (Element)node; text += "<" + el.getTagName(); NamedNodeMap attList = el.getAttributes(); int length = attList.getLength(); for( int j = 0; j < attList.getLength(); j++ ) { Attr att = (Attr)attList.item( j ); text += " " + att.getName() + "='" + att.getValue() + "'"; } text += ">"; text += extractMarkupAsText( el.getChildNodes() ); text += "</" + el.getTagName() + ">"; } if( node instanceof Text ) { text += ((Text)node).getData(); } } } return text; }

Следующие три метода, показанные в листинге 6.39, используются для отображения различных типов данных. Пустые объекты типа String, Integer и Float отображаются как null, а мы не хотели бы, чтобы пользователям приходилось видеть это на экране. Поэтому мы используем приведенные ниже вспомогательные методы, которые заменяют nul 1 на пустую строку.

Листинг 6.39. Методы notNull (Util.java)

static public String notNull( String s ) { if( s == null ) { return ""; } return s; }

static public String notNull( Integer i ) { if( i == null ) { return ""; } return i.toString(); }

static public String notNull( Float f ) { if( f == null ) { return ""; } return f.toString(); }

Следующий ряд методов, показанный в листинге 6.40, используется для преобразования строки, введенной пользователем (то есть объекта String), в некоторое значение. Мы хотим быть уверены, что, если пользователь оставил какую- либо форму незаполненной, мы получим значение null.

Листинг 6.40. Преобразование строки в значение (Util.java)

static public int getInt( String s ) { if( s == null || s.equals( "" ) ) { return 0; } return Integer.parseInt( s ); }

static public Integer getInteger( String s ) { if( s == null || s.equals( "" ) ) { return null; } return new Integer( s ); }

static public float getFloat( String s ) { if( s == null || s.equals( "" ) ) { return 0.0f; } return Float.parseFloat( s ); }

static public double getDouble( String s ) { if( s == null || s.equals( "" ) ) { return 0.0; } return Double.parseDouble( s ); }

}






Класс XMLWriter


Класс XMLWriter — это служебный класс, используемый для записи выходных данных в формате XML. Этот класс записывает данные в выходной поток Output - Stream, который задается конструктором. OutputStream является полем объекта класса XMLWriter. Данный класс также вставляет требуемые разделители строк. В различных операционных системах требуются различные разделители. Например, в Windows применяются символы возврата каретки и перевода строки, а в Unix — только перевода строки. Вам нужен какой-нибудь способ определения, какой их этих разделителей использовать. К счастью, в Java предусмотрено свойство System, которое указывает, какой разделитель применяется в данной операционной системе. В классе XMLWriter определен статический байтовый массив, содержащий это значение.

Класс XMLWriter также отвечает за структурирование отступов в тексте с выходными данными. Определяется байтовый массив, содержащий строку, которая задает расположение отступов. Также вам необходима булева переменная (поле), которая отслеживает, пишем ли мы в данный момент новую строку или продолжаем текущую строку. Начало класса с описанными полями представлено в листинге 6.8.

Листинг 6.8. Начало кода для класса XMLWriter (XMLWriter.java)

package com.XmlEcomBook.Chap06;

import java.io.*;

public class XMLWriter {

static private final byte[] LINE_SEPARATOR = System.getProperty( "line.separator" ).getBytes(); static private final byte[] INDENT = " ".getBytes();

private OutputStream out; private int currentIndent; private boolean newLine = true;

Конструктору XMLWriter в качестве аргумента передается объект OutputStream, в который будут записаны данные. Передавая этот объект, мы можем направить выходной поток XMLWriter в файл, в стандартное устройство вывода или в ответ HTTP. При отладке приложения это свойство очень удобно. Конструктор показан в листинге 6.9.

Листинг 6.9. Конструктор XMLWriter (XMLWriter.java)

public XMLWriter( OutputStream newOut ) { out = newOut; }

Вам необходимо иметь возможность регулировки размера отступа в тексте с выходными данными. Это делается с помощью двух простых методов — один уменьшает размер отступа, а другой увеличивает его. Эти два метода показаны в листинге 6.10.




Листинг 6.10. Регулировка размера отступа (XMLWriter.java)

public void indent() { currentIndent++; }

public void unindent() { currentIndent--; }

Наконец, необходимо осуществить фактическую запись выходных данных. Для этого имеются два метода — в одном после записи выходных данных располагается разделитель строки, а в другом эти данные просто добавляются в конец текущей строки. Эти два метода называются write и write! п. Они аналогичны методам print и println пакета java.io.PrintStream. Когда вы записываете выходные данные, всегда нужно проверять, записываются они в новую строку или нет. Если данные записываются в новую строку, нужно сделать отступ требуемого размера. Метод write! n просто повторяет метод write, а затем размещает символ разделителя строк. Эти методы приведены в листинге 6.11.



Листинг 6.11. Запись выходных данных (XMLWriter.java)

public void write( String s ) throws IOException { if( newLine ) { for( int i = 0; i < currentIndent; i++ ) { out.write( INDENT ); } } out.write( s.getBytes() ); newLine = false; }

public void writeln( String s ) throws IOException { write( s ); out.write( LINE_SEPARATOR ); newLine = true; } }






Код для представления информации пользователю


Теперь нам нужен код для представления пользователю всей информации, содержащейся в классах данных. Необходимо также обеспечить пользователю возможность создавать, редактировать и удалять элементы каталога. Это делается с помощью кода HTML, JSP-страниц и сервлетов. JSP-страницы и сервлеты генерируют код HTML, который отображается в браузере пользователя. Для работы с каталогом пользователь выбирает HTML-страницу, которая обеспечивает возможности создания, редактирования и удаления элементов каталога. На этой странице есть ссылки на сервлет, вызывающий JSP-страницу, которая представляет редактируемую форму HTML, предназначенную для того, чтобы пользователь вносил в нее новую информацию о товарах или изменял старую.

Всю оставшуюся часть этой главы мы посвятим изучению кода HTML, JSP- страниц и сервлетов, которые обеспечивают эту функциональность.

 



Объекты данных


Иногда взаимодействие с документом XML на уровне анализатора оказывается непростым делом. Если документ имеет достаточно сложную структуру, то работа с каждым элементом и атрибутом по отдельности превращается в чреватое многочисленными ошибками кропотливое, длительное занятие. Так получается потому, что анализаторы DOM или SAX не знают о деталях устройства схем отдельных документов. Эти анализаторы общего назначения должны уметь читать любой документ независимо от используемого определения DTD. Было бы гораздо проще, если бы существовали объекты, имеющие такую же структуру, как и тот документ XML, который они анализируют.

Процесс создания набора объектов, предназначенных для преобразования данных XML к более удобным объектам Java, называется связыванием данных (data binding). Связывание данных позволяет работать с исходными данными XML на уровне структуры документа, вместо того чтобы непосредственно обращаться к элементам и атрибутам. Когда набор объектов для связывания данных готов, использующий их программист может даже не подозревать, что основу этих объектов составляет XML.

В этой главе показано, как вручную создать объекты для связывания данных из нашего XML-каталога, но эти объекты можно также генерировать автоматически. Для этой цели имеется множество инструментальных средств. Для создания кода эти программы считывают DTD, схему XML или образец документа XML. Обычно для каждого элемента XML создается свой класс, а поля этого класса отводятся под атрибуты элемента XML.

В качестве примера приведем фрагмент DTD для описания адреса:

<!ELEMENT person (address)>

<!ATTLIST person firstname CDATA #IMPLIED>

lastname CDATA #IMPLIED> <!ELEMENT address (Street, City, Zip)>

Инструмент связывания данных может создать следующий класс для элемента person:

class Person {

private String lastname;

private String firstname: private Address address;

public Address getAddress()

{...} public void setAdress( Address a )

{...} public String getFirstname( )


{...} public void setFistname( String s )

{...} public String getLastname( )

{...} public void setLastnarae( )

{...} }

Также можно создать класс Address, в котором имеются методы, чтобы задавать и извлекать названия улицы, города и почтовый индекс. Как видно, эти классы гораздо удобнее при программировании, чем классы DOM или SAX.



ПРИМЕЧАНИЕ

Одним из таких инструментов связывания данных является Castor, созданный группой Exolab. Более подробная информация содержится на сайте http://castor.exolab.org.

Создание этих классов вручную имеет кроме огромной пользы в плане обучения и другие преимущества. Полученный код будет более эффективным, чем код, генерируемый инструментом связывания данных общего назначения. Большинство инструментов связывания данных используют некоторую объектную модель для загрузки данных и затем создают объекты на ее основе. В результате этого при загрузке документов XML требуется большой объем памяти. Другим преимуществом ручного кодирования классов является возможность учитывать специфические требования каждого приложения. При ручном кодировании классов для связывания данных также достигается большая гибкость.

При разработке классов для связывания данных необходимо изучить DTD каталога и решить, для каких элементов нам требуется создать классы, а какие могут быть представлены стандартными классами, такими как String или Integer. Это решение зависит от того, насколько сложным является элемент. Элементы с содержимым типа PCDATA, лишенные атрибутов, легко могут быть представлены в виде Srting или Integer в зависимости от конкретного содержимого. Для таких простых элементов не нужно создавать новые классы. Но если у элемента имеется дочерний элемент или атрибут, простой класс String может оказаться недостаточным для представления этого элемента, и для него вам потребуется создать новый класс.





Рис. 6.2. UML-диаграмма классов

Посмотрев на DTD из главы 2, вы обнаружите довольно много элементов, не представимых простыми классами. Элементы catalog, product_1ine, product, image, clip и onsale_date содержат дочерние элементы, так что все они являются кандидатами на новые классы. Некоторые другие элементы являются простыми, так как не имеют атрибутов и содержат простые данные. Элементы author, arti st, quantity_in_stock и все дочерние элементы onsale_date являются простыми элементами и могут быть представлены классам String или Integer. Мы не упомянули здесь еще несколько элементов, таких как description (и его дочерние элементы) и price, которые мы будем обрабатывать несколько иначе. Позже мы рассмотрим способ их обработки более подробно.



На рис. 6.2 представлена UML-диаграмма, изображающая классы, которые нам нужно создать. Служебные классы не представлены на этой диаграмме, так как иначе она стала бы слишком сложной.



ПРИМЕЧАНИЕ

UML (Unified Modeling Language) — унифицированный язык моделирования. Это стандартный способ схематического изображения объектно-ориентированных конструкций. Полезно иметь стандартный способ документирования независимо от того, создаете ли вы формальный документ или просто набрасываете схему на листке бумаги. Стандарт помогает быстро и легко сообщать окружающим ваши идеи. Более полная информация по UML находится по адресу www.rational.com/uml.

Если вы внимательно изучите программы, находящиеся на прилагаемом к книге компакт-диске, вы увидите, что каждый класс объектов данных имеет методы toString. Эти методы очень полезны при отладке, но не являются составной частью классов, поэтому не включены в описание программ, чтобы в этой главе вы сосредоточились на основной части кода.






Общие принципы редактирования каталога


Так как данные в нашем каталоге представлены в виде кода XML, одним из способов редактирования является использование обычного текстового или XML- редактора. XML — это формат, доступный для чтения людьми, так что пользователь может просто открыть файл .xml в текстовом редакторе и вручную его отредактировать. Этот способ имеет, однако, два недостатка. Во-первых, редактирование файла XML с помощью стандартного текстового редактора может вызвать некоторые затруднения. Во-вторых, если сервер, формирующий XML, преобразовал файл XML в формат базы данных, то текстовые редакторы уже неприменимы. Для преодоления этих недостатков вы можете создать свой, пользовательский редактор каталогов.

Существует много способов написания такого редактора. Например, вы можете использовать приложение Java, считывающее данные XML и представляющее их с помощью стандартного интерфейса Swing GUI (Graphical User Interface — графический пользовательский интерфейс), в котором пользователь может редактировать эти данные. Если вам нужно распределенное решение, вы можете создать апплет, который будет работать в браузере. В настоящей главе мы создадим редактор, который будет использовать web-сервер, HTML и JSP, так же как было сделано в главе 3 при разработке системы представления каталога. Применение схожих технологий и инструментальных средств в рамках одного проекта имеет большое значение и позволяет упростить разработку и поддержку приложения. В разработке различных частей проекта могут участвовать одни и те же программисты, и, кроме того, есть вероятность многократного использования некоторых идей и фрагментов кода.

Для редактирования каталога мы создадим приложение, которое будет работать через браузер. Такое приложение позволит каждому, у кого имеется доступ к сайту магазина, редактировать каталог. Вы можете создавать JSP-страницы с формами HTML, которые пользователи смогут заполнять. Данные, внесенные в такие формы, будут отправляться на сервер, и, таким образом, можно будет создавать, редактировать и удалять информацию о товарах в каталоге.


Это решение включает в себя четыре уровня, начиная с данных XML и кончая браузером, который отображает информацию. Два промежуточных уровня подразумевают написание программ. К одному из них относятся объекты данных, которые осуществляют преобразование между XML и Java, а в другом создаются и интерпретируются данные HTML, отображаемые браузером. На рис. 6.1 схематично изображены эти уровни.





Рис. 6.1. Общие принципы решения задачи редактирования каталога

Первым шагом в создании приложения является конструирование объектов XML для представления информации каталога. В этих объектах будут содержаться данные о сериях товаров и об отдельных товарах, а также такие атрибуты информации об отдельных товарах, как их изображения (файлы с расширением .gif) или видеоклипы. Эти объекты конструируются на основе исходных данных XML, могут отображаться в формате XML и служат мостом между миром XML и миром объектов Java.

Затем вы создадите сервлеты и JSP-страницы, которые будут отображать информацию для пользователя и обрабатывать введенную им информацию. При этом информация будет извлекаться из объектов данных и с помощью браузера отображаться в формах. После заполнения форм пользователем информация отправится обратно на сервер. На этом этапе также потребуется программа, которая получит эту информацию и обновит объекты данных.

 




Сервлет UpdateProduct


Сервлет UpdateProduct вызывается из JSP-страницы Edit, когда пользователь заканчивает ввод в форму информации о товаре. Задача этого сервлета заключается в том, чтобы собрать данные из параметров запроса (объекта request) и обновить объект связывания данных Product. После некоторой стандартной инициализации в методе doGet из текущего объекта HttpServletRequest извлекается объект Session. Затем из него вы получаете объект Catalog, с которым и работаете в данном сеансе. Из каталога вы получаете объект Product, снабженный правильным идентификатором. Все это иллюстрирует листинг 6.58.

Листинг 6.58. Начало кода сервлета UpdateProduct (UpdateProduct.java)

import java.io.*; import javax.servlet.*; import javax.servlet.http.*;

import com.XmlEcomBook.Chap06.*;

public class UpdateProduct extends HttpServlet {

public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { res.setContentType("text/html"); PrintWriter out = res.getWriter();

HttpSession session = req.getSession(); Catalog catalog = (Catalog)session.getAttribute( "catalog" ); String id = req.getParameter( "id" ); Product product = catalog.getProduct( id );

Далее, как показано в листинге 6.59, проверяется, не является ли элемент product пустым, то есть не равен ли он null. Если это так, то мы делаем вывод, что была выполнена операция Add Product, поэтому требуется добавить новый товар в каталог, то есть создать новый объект Product с правильным значением идентификатора и добавить его к соответствующей серии товаров. Затем из запроса считываются такие параметры, как имя и ключевые слова, и в объект Product вносятся обновленные значения этих параметров.

Листинг 6.59. Создание нового объекта product, обновление ключевых слов и имени (UpdateProduct.java)

if( product == null ) { //new product product = new Product(); product.setId( id ); String productLineString = req.getParameter( "productline" ); ProductLine productLine = catalog.getProductLine( productLineString ); productLine.addProduct( product ); } String keywords = req.getParameter( "keywords" ); product.setKeywords( keywords ); String name = req.getParameter( "name" ); product.setName( name );


Далее внесенные в форму объекты Author добавляются в Product. Прежде чем вы впишете новых авторов, необходимо удалить все существующие на данный момент записи об авторах. Для этого используется метод removeAHAuthors. После этого все готово для добавления новых авторов. Для этого используется метод getAuthor, определенный ниже в этом разделе. Метод getAuthor возвращает булеву величину true, если им был обнаружен автор с указанным идентификатором, и false, если не было найдено ни одного автора с таким идентификатором. Также вам нужно проверить, не был ли добавлен какой-либо новый автор; для этого используется идентификатор New. Далее весь процесс повторяется для объектов Artist, как показано в листинге 6.60.

Листинг 6.60. Добавление Author и Artist (UpdateProduct.java)

product.removeAllAuthors(); for( int i = 0; getAuthor( "" + i, req, product ); i++ ) ;//do nothing getAuthor( "New", req, product );

product.removeAllArtists(); for( int i = 0; getArtist( "New", req, product ); i++ ) ;//do nothing getArtist( "New", req, product );

Для обновления остальных характеристик товара используется код, во многом похожий на уже рассмотренный. Он приведен в листинге 6.61. С помощью метода getParameter данные извлекаются из объекта request, а затем используется соответствующий метод set объекта Product. Для двух полей, clip и image, задействован тот же процесс, который мы рассматривали для полей artists и authors. Наконец, создается и отображается для пользователя HTML-страница, которая сообщает, что обновление данных прошло успешно, и на которой имеется ссылка на главную страницу.

Листинг 6.61. Добавление остальных характеристик товара (UpdateProduct.java)

String price = req.getParameter( "price" ); price = price.replace( '$', ' ' ); product.setPrice( Util.getDouble( price ) ); String quantity = req.getParameter( "quantity" ); product.setQuantityInStock( Util.getInt( quantity ) ); String dateString = req.getParameter( "onSaleDate" ); Date date = product.getOnSaleDate(); if( date == null ) { date = new Date(); product.setOnSaleDate( date ); } date.fromString( dateString ); String description = req.getParameter( "description" ); product.setDescription( description );



product.removeAllImages(); for( int i = 1; getImage( "" + i, req, product ); i++ ) ;//do nothing getImage( "New", req, product );

product.removeAllClips(); for( int i = 1; getClip( new String( "" + i ), req, product ); i++ ) ;//do nothing getClip( "New", req, product ); // Return HTML. out.println( "<html><head><title>Update Successful</title> </head>" ); out.println( "<body><h2>Update Succesful</h2>" ); out.println( "<a href='/main.html'>Return to main page</a> </body></html>" ); }

Метод getAuthor, показанный в листинге 6.62, вызывается из метода doPost cep- влета, чтобы извлекать информацию из запроса и добавлять ее в Product. В JSP- странице Edit мы идентифицировали каждый элемент Author с помощью значения счетчика, которое добавлялось к строке author, а теперь мы ищем ту же строку для извлечения информации. Если строка не найдена или значение элемента Author пропущено, возвращается f al se. Если же значение обнаружено, то оно добавляется в Product как имя автора и метод возвращает булеву величину true.

Листинг 6.62. Метод getAuthor (UpdateProduct.java)

boolean getAuthor( String i, HttpServletRequest req, Product product ) { String author = req.getParameter( "author" + i ); if( author == null || author.equals( "" ) ) { return false; } product.addAuthor( author ); return true; }

Методы getArtist, getlmage и getClip, показанные в листинге 6.63, используют ту же технику, что и в методе getAuthor. Метод getArtist почти идентичен getAuthor, а методы getlmage и getClip несколько длиннее, так как для объектов Image и Clip требуется извлечь большее количество параметров запроса и установить их в качестве значений полей объекта Product.

Листинг 6.63. Методы getArtist, getlmage и getClip (UpdateProduct.java)

boolean getArtist( String i, HttpServletRequest req, Product product ) { String artist = req.getParameter( "artist" + i ); if( artist == null || artist.equals( "" ) ) { return false; } product.addArtist( artist ); return true; }



boolean getImage( String i, HttpServletRequest req, Product product ) { String format = req.getParameter( "img" + i + "-format" ); if( format == null || format.equals("") ) { return false; } Image img = new Image(); img.setFormat( format ); String src = req.getParameter( "img" + i + "-src" ); img.setSrc( src ); String height = req.getParameter( "img" + i + "-height" ); img.setHeight( Util.getInteger( height ) ); String width = req.getParameter( "img" + i + "-width" ); img.setWidth( Util.getInteger( width ) ); String caption = req.getParameter( "img" + i + "-caption" ); img.setCaption( caption ); product.addImage( img ); return true; }

boolean getClip( String i, HttpServletRequest req, Product product ) { String format = req.getParameter( "clip" + i + "-format" ); if( format == null || format.equals("") ) { return false; } Clip clip = new Clip(); clip.setFormat( format ); String src = req.getParameter( "clip" + i + "-src" ); clip.setSrc( src ); String title = req.getParameter( "clip" + i + "-title" ); clip.setTitle( title ); String length = req.getParameter( "clip" + i + "-length" ); clip.setLength( length ); String size = req.getParameter( "clip" + i + "-size" ); clip.setSize( size ); String description = req.getParameter ( "clip" + i + "-description" ); clip.setDescription( description ); product.addClip( clip ); return true; } }

JSP-страницы и сервлеты, представленные в этой главе, показывают, как можно использовать приложение, работающее через браузер, для добавления, редактирования и удаления элементов каталога, размещенного на сервере. Таким образом, возможности WWW расширяются от простого представления статических данных до полноценной системы обработки информации.

Представленное в этой главе решение лишено некоторых свойств, которые потребовались бы для настоящего крупномасштабного сайта. Следовало бы улучшить обработку ошибок, которая не слишком хорошо организована. У нас отсутствует синхронизация доступа к файлам XML, так что если два человека одновременно возьмутся редактировать один и тот же файл, то изменения, внесенные одним из них, будут потеряны. Но, несмотря на эти недостатки, данное приложение справилось бы с задачей редактирования XML-каталога товаров небольшого сайта.


Класс для создания файлов снимков


Создание файлов снимков осуществляется классом PrepQxml, как показано в листинге 7.24 и следующих листингах. Конструктор берет объект XML Document и извлекает имена выходных файлов, которые являются значениями атрибута file в тегах Questionnaire и Terminal. Этот класс используется в сервлете, выполняющем анализ результатов (этот сервлет рассматривается далее в этой главе), но может использоваться также и в других процессах.

Листинг 7.24. Код класса PrepQxml (PrepQxml.java)

package com.XmlEcomBook.Chap07;

import com.XmlEcomBook.DOMlibrary ; import org.w3c.dom.* ; import com.sun.xml.tree.* ; import java.io.*; import java.util.* ;

public class PrepQxml { public int state = 1 ; Document doc ; public String primaryfile ; public String title ; public String author ; public String date ; String[] files ; Vector allfiles = new Vector() ; Hashtable prepHash = new Hashtable() ;

PrepQxml( Document d ){ doc = d ; Element E = doc.getDocumentElement(); primaryfile = E.getAttribute("file"); allfiles.addElement( primaryfile ); title = E.getAttribute("title"); author = E.getAttribute("author"); date = E.getAttribute("date"); NodeList terminals = E.getElementsByTagName("Terminal"); int ct = terminals.getLength(); // this locates any output files created by <Terminal> tags for( int i = 0 ; i < ct ; i++ ){ E = (Element)terminals.item(i); String tmp = E.getAttribute("file"); if( tmp.length() > 0 ) allfiles.addElement( tmp ); } }

public String[] getFiles(){ return files ; }

Для каждого выходного файла из опроса метод createFiles, показанный в листинге 7.25, вызывает метод makeXML, чтобы создать файл, у которого имеется открывающий и закрывающий теги <QResu1tSet>, необходимые для создания корневого элемента. Имя этого файла получается добавлением символов FMT к имени файла с результатами.

Листинг 7.25. Методы createFiles и makeXML (PrepQxml.java)

// for every file in allfiles, create a temporary with xml root // return array of file path/names public String[] createFiles() throws IOException { files = new String[ allfiles.size() ]; int n = 0 ; Enumeration e = allfiles.elements(); while( e.hasMoreElements() ){ String tmp = (String)e.nextElement(); files[n++] = tmp ; System.out.println("Create temporary for " + tmp ); prepHash.put( tmp, makeXML( tmp ) ); } return files ; } // fn is the name of the answers file, return the name of // the formatted file with root for creation of DOM public String getAnsXml( String fn ){ return (String) prepHash.get( fn ); } // this creates a complete XML document by adding a root // element to the Questionnaire output file contents private String makeXML(String fn )throws IOException { File inf = new File( fn ); BufferedReader read = new BufferedReader ( new FileReader( inf )); int p = fn.lastIndexOf('.'); String outFN = fn.substring( 0,p ) + "FMT" + fn.substring(p); File outf = new File( outFN ); BufferedWriter bw = new BufferedWriter ( new FileWriter( outf ), 4096); PrintWriter write = new PrintWriter( bw ); write.println( "<?xml version=\"1.0\" standalone=\"yes\" ?>"); write.println( "<!-- formatted Questionnaire results -->"); write.println( "<QresultSet title=\"" + title + "\" >"); String tmp = read.readLine(); int ct = 1 ; while( tmp != null ){ write.println( tmp ); tmp = read.readLine(); } read.close(); write.println("</QresultSet>"); write.close(); System.out.println("Created " + outFN ); return outFN ; }

public String toString() { StringBuffer sb = new StringBuffer("PrepQxml title: "); sb.append( title ); sb.append(" author: "); sb.append( author ); sb.append(" date: " ); sb.append( date ); sb.append(" primary output: "); sb.append( primaryfile ); sb.append(" other files: " ); sb.append( "none"); return sb.toString(); }

}

 



Класс для создания таблиц


Класс TallyQues использует интерфейс SAX для обработки документа QResultsSet. В результате этой обработки формируется таблица, в которой указывается, сколько раз встретился тот или иной ответ на данный вопрос. Выбор интерфейса SAX в данном случае очевиден — количество занимаемой памяти не зависит от общего числа ответов.

В листинге 7.26 показаны инструкции импорта, объявления классов, переменных и конструктор для класса TallyQues. Заметим, что мы основываем этот класс на классе Handler-Base пакета org.xml.sax, потому что в нем по умолчанию предусмотрены методы обработки событий. Все, что нам остается сделать, — переписать встроенные обработчики событий так, чтобы они обрабатывали те события, которые представляют для нас интерес.

Для работы класса TallyQues используется хэш-таблица, в которой каждому варианту ответа из полного набора вариантов по всем вопросам отводится свой элемент. Эти элементы являются экземплярами внутреннего класса Counter (см. листинг 7.30). Ключом для каждого элемента является атрибут id тега Ques вместе с атрибутом val тега Qopt.

Также у нас имеется вектор Vector с названием ordered (в котором хранится информация по всем вопросам в том порядке, в котором они следуют в анкете) и хэш-таблица qtext (которая содержит строки Qtext с текстом вопросов, снабженные атрибутами id тегов Quest в качестве ключей). Конструктор использует объект Document, соответствующий нашей анкете, для создания всех этих объектов.

Листинг 7.26. Начало кода класса TallyQues (TallyQues.java)

package com.XmlEcomBook.Chap07;

import java.io.* ; import java.util.* ; import org.w3c.dom.* ; import org.xml.sax.* ; import org.xml.sax.helpers.ParserFactory ; import com.sun.xml.parser.Resolver ;

/* org.xml.sax.HandlerBase is a convenience class that extends java.lang.Object and implements the SAX interfaces implements EntityResolver, DTDHandler, DocumentHandler, ErrorHandler */ public class TallyQues extends HandlerBase { static public String parserClass = "com.sun.xml.parser.Parser" ;


private Hashtable tally = new Hashtable(); // Counters keyed by unique // ordered has a Vector of Counters per question private Vector ordered = new Vector(); private Hashtable qtext = new Hashtable(); // <Qtext> by id

public String tableStyle = "align=\"center\" border=\"3\" " ; public String lastErr = null ; public int resultCt = 0 ; String id ; // <Ques> attribute "id" as detected during parse

// constructor creates the vectors and hashtables to store results // qd is the questionnaire source XML doc public TallyQues( Document qd ){ Element E = qd.getDocumentElement(); NodeList qnl = E.getElementsByTagName("Ques"); int ct = qnl.getLength(); for( int i = 0; i < ct ; i++ ){ Vector quesv = new Vector(); // for this <Ques> ordered.addElement( quesv ); E = (Element)qnl.item(i); // Element is a <Ques> NodeList txn = E.getElementsByTagName("Qtext"); String tx = txn.item(0).getFirstChild().getNodeValue(); // question text String id = E.getAttribute( "id" ); qtext.put( id, tx ); quesv.addElement( id ); // first element of quesv is the id NodeList opt = E.getElementsByTagName("Qopt"); int opct =opt.getLength(); for( int n = 0 ; n < opct ; n++ ){ Element opE = (Element) opt.item(n); String val = opE.getAttribute("val"); String text = opE.getFirstChild().getNodeValue(); Counter cntr = new Counter( id, val, text ); quesv.addElement( cntr ); tally.put( cntr.unique, cntr ); } } }

Обработка снимка опроса

Фактическая обработка файла, содержащего снимок результатов опроса, начинается с метода tallyAns. Как показано в листинге 7.27, работа этого метода состоит из следующих этапов.

Файл открывается как объект org.xml .InputSource.

Создается анализатор в соответствии со строкой parserClass (см. листинг 7.26). Мы используем анализатор из пакета Sun, но вы можете заменить его на любой подходящий вам анализатор.

Объект TallyQues присоединяется к анализатору, с тем чтобы он мог получать вызовы методов обработки событий.



Вызывается метод parse и начинается анализ.

Если не происходит никаких ошибок, то по выполнении метода parse таблица готова. Если происходит ошибка, метод tallyAns возвращает null, а если все прошло успешно, то возвращается переменная ordered.



Листинг 7.27. Метод tallyAns (TallyQues.java)

// srcdoc is complete path to a formatted answer set file public Vector tallyAns(String srcdoc ){ Parser parser ; InputSource input ; try { File f = new File( srcdoc ); input = Resolver.createInputSource( f ); parser = ParserFactory.makeParser( parserClass ); parser.setDocumentHandler( this ); System.out.println("Start parse"); parser.parse( input ); }catch(SAXParseException spe){ StringBuffer sb = new StringBuffer( spe.toString() ); sb.append("\n Line number: " + spe.getLineNumber()); sb.append("\nColumn number: " + spe.getColumnNumber() ); sb.append("\n Public ID: " + spe.getPublicId() ); sb.append("\n System ID: " + spe.getSystemId() + "\n"); lastErr = sb.toString(); ordered = null ; }catch(Exception e){ lastErr = e.toString(); ordered = null ; } return ordered ;

Обработка событий с помощью интерфейса SAX

Теперь рассмотрим метод обработки событий, относящийся к методам интерфейса SAX и представленный в листинге 7.28. Из всех методов интерфейса SAX нам нужен только один — startElement. Для каждого тега Ques мы получаем значение атрибута id, которое используется вместе с тегами Qopt, содержащими варианты ответов на вопрос Ques Для каждого тега Qopt мы создаем строку, которая объединяет значение атрибута id вопроса со значением атрибута val элемента Qopt (варианта ответа) Эта строка используется как ключ для извлечения из хэш-таблицы tally соответствующего объекта класса Counter Таким образом подсчитывается, сколько раз встретился данный вариант ответа на данный вопрос анкеты



Листинг 7.28. Методы обработки событий SAX (TallyQues.java)

// this is the SAX specified "callback" called when the // parser detects an element public void startElement( String name, AttributeList attrib) throws SAXException { if( name.equals("Ques") ){ id = attrib.getValue("id"); } else { if( name.equals("Qopt") ){ String unique = id + ":" + attrib.getValue("val"); Counter cntr = (Counter)tally.get( unique ); if( cntr != null ) cntr.countIt(); } else { if( name.equals("Qresults"))resultCt++ ; } } }



Чтобы объединить всю статистическую информацию, полученную классом TallyQues, используется вектор ordered, в котором для каждого вопроса отведен свой элемент, причем эти элементы следуют в том же порядке, в котором расположены вопросы в исходном XML-сценарии анкеты Каждый такой элемент сам по себе также является вектором, в котором содержится идентификатор вопроса id (строка), а затем следуют объекты класса Counter для каждого из вариантов ответа В хэш-таблице qtext указан текст каждого вопроса, ключом к которому является идентификатор данного вопроса

Форматирование полученных результатов

Рассмотрим теперь, каким образом отображается полученная совокупность данных Метод formatAlly, приведенный в листинге 7 29, выводит для каждого вопроса HTML-таблицу, придерживаясь исходного порядка расположения элементов На рис 7 2 показана одна из таблиц, сформированная в результате опроса, который мы недавно проводили на нашем web-сайте в связи с экзаменом на получение сертификата программиста на Java



Листинг 7.29. Метод formatTally создает таблицу HTML (TallyQues.java)

// assumes that tallyAns was just run public void formatTally(PrintWriter out ){ out.println("<center><h2>" + ordered.size() + " Questions " + resultCt + " Responses</h2></center>"); Enumeration e = ordered.elements(); while( e.hasMoreElements() ){ Vector v = (Vector) e.nextElement(); String id = (String)v.firstElement(); out.println("<center><h2>Question: " + id + "</h2>"); out.println("<p>" + qtext.get(id) + "</p>" ) ; out.println("<table cols=\"3\"" + tableStyle + " >"); out.print("<tr>"); out.print("<th>Val</th><th>Count</th><th>Short Option Text</th>"); out.println("</tr>"); for( int i = 1 ; i < v.size(); i++ ){ Counter c = (Counter) v.elementAt(i); out.print("<tr><td>" + c.val + "</td>"); out.print("<td>" + c.count + "</td>" ); out.println("<td>" + c.text + "</td></tr>"); } out.println("</table></center><br><hr>"); } }



public String toString() { StringBuffer sb = new StringBuffer("TallyQues "); return sb.toString() ; }





Рис. 7.2. Отображение в браузере таблицы, сформированной методом fbrmatTally

Остается рассмотреть еще один компонент класса TallyQues — внутренний класс Counter. Как показано в листинге 7.30, в объект Counter входят идентификатор вопроса, значение атрибута val данного варианта ответа и текст варианта ответа. Этот текст мы ограничили по длине, чтобы таблица оставалась компактной, но вы легко можете снять это ограничение.



Листинг 7.30. Внутренний класс Counter (TallyQues.java)

// counter objects represent a single question/option combo class Counter { public String val ; public String unique ; // <Ques id plus ":" plus <Qopt val public String text ; // the first counterTextLen chars public int count = 0 ;

Counter( String id, String v, String tx ){ val = v ; unique = id + ":" + val ; if( tx.length() > counterTextLen ) { text = tx.substring(0, counterTextLen); } else { text = tx ; } } public void countIt(){ count++ ; }

public String toString(){ return "ID: " + unique + " " + count + " " + text ; } } }

Классы PrepQxm и TallyQues, рассмотренные в предыдущих разделах, можно по-разному использовать для создания таблиц в формате HTML. В нашем случае мы задействуем сервлет QanalyslsServ, описанный в следующем разделе.






Класс Interpreter


В классе Interpreter инкапсулирована вся работа по созданию на основе документа XML форм для вопросов. Этот класс устроен так, чтобы быть максимально гибким в отношении способов анкетирования. Хотя в данной версии поддерживаются только два типа вопросов, используемый механизм может быть легко расширен для включения в анкету других типов вопросов.

Статические методы и инструкции импорта для класса Interpreter показаны в листинге 7.9. Константы QMC и QMCM означают два допустимых в нашей версии типа вопросов: QMC — вопрос, допускающий выбор одного ответа из списка, а QMCM — вопрос, допускающий выбор нескольких ответов из списка. Рассмотрим в качестве примера код XML для вопроса о музыкальных предпочтениях, приведенный в листинге 7.8.

Листинг 7.8. Начало блока вопросов в документе XML (customersurvey.xml)

<Block name="cds" type="terminal" > <Ques type="QMCM" id="palm:1"> <Qtext>Please select all of the categories of CD that you would like to see in our catalog </Qtext> <Qopt val="0">Classical music</Qopt> <Qopt val="1">Country and Western</Qopt> <Qopt val="2">The latest Pop Groups</Qopt> <Qopt val="3">Current Rock</Qopt> <Qopt val="4">Golden Oldies Rock</Qopt> <Qopt val="5">Environmental</Qopt> <Qopt val="6">Novelty and Humor</Qopt> </Ques>

Открывающий тег Ques использует атрибут type для задания типа вопросов, в данном случае QMCM. Атрибут id является уникальным идентификатором этого вопроса.

Вместо того чтобы сравнивать строку, являющуюся значением атрибута type, с возможными типами вопросов, мы используем хэш-таблицу Hashtable и отыскиваем в ней целочисленное значение типа int, которое можно использовать в инструкции switch для выбора нужного способа представления вопроса. Эта хэш-таблица называется typeHash, а поиск в ней осуществляется с помощью метода 1 ookUpType, показанного в листинге 7.9.


Для добавления нового типа вы просто должны будете определить новую строку String и целочисленную константу типа int в качестве статических переменных класса Interpreter и в хэш-таблице typeHash.

Листинг 7.9. Константы и инструкции импорта в начале исходного кода класса Interpreter (Interpreter.java)

package com.XmlEcomBook.Chap07; // import org.w3c.dom.* ; import com.sun.xml.tree.* ; import java.io.*; import java.util.* ; import javax.servlet.*; import javax.servlet.http.*;

public class Interpreter { static final String brcrlf = "<br>\r\n"; static final int QMC = 1 ; static final int QMCM = 2 ;

static Hashtable typeHash = new Hashtable(); static { // static initialization block typeHash.put("QMC", new Integer( QMC )); typeHash.put("QMCM", new Integer( QMCM )); }

static int lookUpType( String type ){ Integer N = (Integer)typeHash.get( type ); if( N == null ) return 0 ; return N.intValue(); }

В листинге 7.10 приводятся переменные экземпляра и конструктор класса Interpreter. Для каждого сеанса работы пользователя создается экземпляр класса Interpreter, в котором хранится сам документ и отмечается текущая позиция пользователя в процессе его «продвижения» по анкете. Переменные nowBlock и nowNode являются ссылками на объекты, реализующие интерфейс org.w3c.dom.Node.



Листинг 7.10. Переменные экземпляра и конструктор класса Interpreter (Interpreter.java)

// instance variables below this Document theDom ; Node nowBlock, nowNode ; // nowNode should be quest type boolean terminal = false ; // true if the block is terminal String title ; String css = "" ; // may change from block to block String actionStr ;

NodeList blockNodeList ; // Nodes that are <Block type // the constructor public Interpreter( Document doc, String handler ){ theDom = doc ; actionStr = handler ; Element E = theDom.getDocumentElement(); // the root blockNodeList = E.getElementsByTagName("Block"); // note that in contrast to other get methods, getAttributes // returns "" if the attribute does not exist title = E.getAttribute("title"); css = E.getAttribute("css"); // used for <Intro> }



Для того чтобы обеспечить некоторую гибкость в форматировании вопросов, предусмотрена возможность задавать каскадную таблицу стилей для всего документа и заменять определенный по умолчанию стиль для каждого блока. Метод writeHead, показанный в листинге 7.11, управляет выводом начала HTML-страницы и включает в себя ссылку на таблицу стилей, если она применяется. В этом листинге также показаны методы startForm и endForm. Заметим, что переменная quesid записывается в форму как скрытая переменная, которая впоследствии извлекается в методе doPostQ (листинг 7.16).



Листинг 7.11. Методы для создания различных частей HTML-страницы (Interpreter.java)

void writeHead( PrintWriter out ){ out.println("<html>"); out.println("<head><title>" + title + "</title></head>"); if( css.length() > 0 ){ out.println("<link href=\"" + css + "\" type=\"text/css\" rel=\"stylesheet\">" ); } out.println("<body>"); }

// assumes nowNOde is set to the first question // output form start and question text public void startForm(PrintWriter out ){ out.print("<form method=\"POST\" action=\"" ); out.print( actionStr ); out.println("\" >"); }

// fills in hidden variable and button public void endForm( PrintWriter out, String id ){ out.print("<input type=\"hidden\" name=\"quesid\" value=\"" + id + "\" ><br>" ); out.print("<input type=\"submit\" value=\"" ); out.print("Next" ); out.println("\" name=\"action\" ><br>"); out.println("</form><br>"); }

 




Класс Recorder


Класс Recorder отвечает за хранение ответов пользователя на каждый вопрос и за их запись в файл для дальнейшего анализа. Каждому пользователю отводится экземпляр класса Recorder, который связан с данным сеансом и записывает ответы только этого пользователя.

Корневым элементом документа XML является тег Questionnaire, у которого имеется несколько атрибутов, используемых классом Recorder. Как показано в следующем примере тега Questionnaire, эти атрибуты называются title, author, date, method и file:

<Questionnaire title="Survey 1" author="WBB" date="May 19, 2000" method="xml" file="e:\scripts\questionnaire\test result.xml" >

Атрибуты title, author и date просто записываются в класс Recorder и отображаются позже, но атрибуты method и file управляют действиями класса Recorder. Мы включили атрибут method для того, чтобы можно было записывать результаты опроса в формате, отличном от XML. Например, если вы предпочитаете использовать программу анализа таблиц, можете добавить методы, которые запишут результаты в виде строки текста, разделенного запятыми. Атрибут file задает путь к стандартному файлу, куда будут записываться результаты, если не назначен другой файл в теге Terminal.

В формате XML, который мы здесь используем, ответы каждого пользователя записываются в тег Qresults, для каждого вопроса отводится тег Ques, а для каждого выбранного ответа — тег Qopt. Конечно, такая структура является более объемной, чем запись в одну строку, но зато она очень гибкая. Получившаяся структура не является законченным документом XML, так как в ней отсутствует корневой элемент. Мы покажем, как решается этот вопрос в разделе «Варианты анализа анкеты».

В листинге 7.20 показаны инструкции импорта, переменные экземпляра и единственная статическая переменная класса Recorder. Статическая переменная filelock является объектом типа String, который используется в инструкции synchronized для того, чтобы гарантировать, что в каждый момент времени только один экземпляр объекта Recorder фактически осуществляет запись в файл.


Листинг 7.20. Начало исходного кода класса Recorder (Recorder.java)

package com.XmlEcomBook.Chap07;

import org.w3c.dom.* ; import com.sun.xml.tree.* ; import java.io.*; import java.util.* ; import javax.servlet.*; import javax.servlet.http.*;

public class Recorder { // this String is used to prevent more than one Recorder from // writing anywhere at the "same time" static String filelock = "RecorderLock" ;

// these are instance variables String userid, usertype, sessionid ; String qresultStr ; String source ; // the xml file String method, output ; // how and where we save Hashtable record ; // one string per response public boolean terminated = false ;

Конструктор класса Recorder, показанный в листинге 7.21, вызывается из метода doGet сервлета. Он задает несколько переменных, характеризующих конкретного пользователя. Также он создает хэш-таблицу record, которая будет использоваться для записи ответов на вопросы.

В листинге 7.21 также показан метод setMethods, который определяет.метод записи результатов и атрибутов файла с помощью документа XML. Также он создает открывающий тег Qresults и сохраняет его в переменной qresultStr для дальнейшего использования.



Листинг 7.21. Конструктор класса Recorder и метод setMethods (Recorder.java)

public Recorder(String id, String typ, String ses,String src ){ userid = id ; usertype = typ ; sessionid = ses ; source = src ; record = new Hashtable(); }

/* method information from <Questionnaire> attributes <Questionnaire title="First Test Quest" author="wbb" date="May 19, 2000" method="xml" file="e:\scripts\questionnaire\testresult.xml" > */ public void setMethods( Document doc ){ NamedNodeMap nnm = doc.getDocumentElement().getAttributes(); method = nnm.getNamedItem("method").getNodeValue(); output = nnm.getNamedItem("file").getNodeValue(); // for xml method StringBuffer sb = new StringBuffer( 50 ); sb.append("<Qresults source=\""); sb.append( source ); sb.append( "\" date=\"" ); sb.append( new Date().toString() ); sb.append( "\" userid=\""); sb.append(userid); sb.append( "\" usertype=\"" ) ; sb.append( usertype ); sb.append( "\" sessionid=\""); sb.append( sessionid ); sb.append("\">\r\n"); qresultStr = sb.toString(); }



В листинге 7. 22 показаны методы terminal, record и toString. Метод terminal отвечает за запись собранных ответов данного пользователя в отведенный для этого файл. Метод toString используется как вспомогательный при отладке.

Метод record вызывается из метода doPost объекта Interpreter после того, как приходит ответ на очередной вопрос. Отметим, что если вам нужно будет создать какой-либо новый тип, требующий специального способа записи, метод record допускает переключение между типами вопросов. Например, если вам потребуется принимать введенный пользователем текст, он будет записан в раздел CDATA, чтобы в случае наличия в этом тексте каких-либо символов, имеющих специальное назначение в XML, не возникло бы затруднений.

При каждом вызове метода record создается строка, содержащая тег Ques, которая записывается в хэш-таблице record и ключом для которой служит переменная quesid. Поскольку хэш-таблица не сохраняет порядок добавления в нее элементов, очередность расположения тегов Ques в ней непредсказуема. Но это не страшно, поскольку для определения порядка следования вопросов мы можем использовать документ XML, содержащий сценарий опроса.



Листинг 7.22. Код класса Recorder, продолжение (Recorder.java)

// called when a <Terminal block is reached // if altdest is not "" this changes the default output public void terminal(String altdest ) throws IOException { if( altdest != null && altdest.length() > 4 ) output = altdest ; if( output == null || output.length() < 5 ){ System.out.println("QARG output is: " + output ); return ; } terminated = true ; // write in append mode synchronized( filelock ){ FileWriter fw = new FileWriter( output,true ); PrintWriter pw = new PrintWriter( fw ); pw.print( qresultStr ); Enumeration e = record.elements(); while( e.hasMoreElements() ){ pw.print( (String)e.nextElement() ) ; } pw.print("</Qresults>\r\n"); pw.close(); } // end synchronized block } // recording format in xml /*<Qresults source=.... date= > <Ques id="start:1"> <Qopt val="a"></Qopt><Qopt val="b"></Qopt> </Ques>



</Qresults> */ public void record( String quesid, int type, String[] optS ){ if( terminated ) return ; // prevent backing up from terminal Q // System.out.println("Start record: " + quesid ); StringBuffer sb = new StringBuffer( 100 ); sb.append("<Ques id=\"" ); sb.append( quesid ); sb.append("\" >"); switch( type ){ case Interpreter.QMC : case Interpreter.QMCM : if( optS == null || optS.length == 0 ) break ; for(int i =0 ; i < optS.length ; i++ ){ sb.append("<Qopt val=\""); sb.append( optS[i] ); sb.append("\"></Qopt>"); } break ; default : sb.append("UNKNOWN TYPE"); } sb.append("\r\n</Ques>\r\n"); String tmp = sb.toString(); // note this will replace answer if user backed up with browser back record.put( quesid, tmp ); return ; }

public String toString() // for debugging { StringBuffer sb = new StringBuffer( "Recorder user: " ); sb.append( userid ); sb.append(" type: " ); sb.append( usertype ); sb.append(" session: " ); sb.append( sessionid );sb.append(" method: "); sb.append( method ); sb.append( " output: " ); sb.append( output ); // how and where we save return sb.toString() ; }

}

В листинге 23 показаны результаты ответа одного пользователя на простой опрос. Атрибут source указывает, какой файл XML использовался для создания анкеты. В атрибут date записывается дата первого вхождения пользователя в систему и открытия страницы введения в анкету. Мы также включили атрибут sessionid для помощи в отладке, но, вероятно, без него можно обойтись.



Листинг 7.23. Запись результатов опроса одного пользователя на XML

<Qresuits source="e:\scripts\javatest.xml" date="Mon May 22 22:30:20 CDT 2000" userid="unknown" usertype="passed" sessionid="9590526208594804">

<Ques id="studying:4" >

<Qopt val="a"></Qopt><Qopt va1="b"></Qopt> <Qopt val="d"></Qopt><Qopt val=*e"> </Qopt> <Qopt val="k"></Qopt><Qopt val="l"> </Qopt> <Qopt val="m"> </Qopt> </Ques>

<Ques id="studying:3"><Qopt val="l"></Qopt> </Ques> <Ques id="studying:2"><Qopt val="2"> </Qopt> </Ques> <Ques id="studying:1" > <Qopt val="a"> </Qopt> <Qopt val="f"></Qopt> </Ques> </Qresults>

 




Код сервлета управления опросом


В листинге 7.4 показаны инструкции импорта, объявления классов и метод init для сервлета QuestionnaireServ. Статическая переменная homedir, значение которой может быть получено из ServletConfig, используется в методе init для считывания файла свойств. С помощью этого файла задается путь к исходному файлу XML для данной анкеты. Также файл свойств может потребоваться для того, чтобы задать значение переменной handler типа Srting. Это значение равно URL- адресу, который используется web-сервером для данного сервлета.

Листинг 7.4. Начало кода сервлета QuestionnaireServ (QuestionnaireServ.java)

package com.XmlEcomBook.Chap07;

import com.XmlEcomBook.DOMlibrary ; import org.w3c.dom.* ; import java.io.*; import java.util.* ; import javax.servlet.*; import javax.servlet.http.*;

public class QuestionnaireServ extends HttpServlet { static String brcrlf = "<br>\r\n" ; static String homedir = "e:\\scripts\\questionnaire" ; static String handler = "http://www.lanw.com/servlet/Questionnaire" ; static String version = "v1.0"; Properties qProp ;

public void init(ServletConfig config) throws ServletException { super.init(config); String tmp = config.getInitParameter("homedir"); if( tmp != null ) homedir = tmp ; System.out.println("Start QuestionnaireServ using " + homedir ); File f = new File( homedir, "questionnaire.properties"); try { qProp = new Properties(); qProp.load( new FileInputStream(f) ); tmp = qProp.getProperty("handler"); if( tmp != null ) handler = tmp ; System.out.println ("Loaded properties for Questionnaire handler: " + handler ); }catch(IOException e){ System.out.println("Error loading " + e ); } }

Сервлет QuestionnaireServ отслеживает взаимодействие с каждым пользователем, отвечающим на вопросы анкеты, в сеансах. Предполагается, что исходный запрос — это запрос методом GET с HTML-страницы, на которой имеется простая форма для определения значения переменной qname, идентифицирующей требуемую анкету. Приспосабливая этот сервлет для своего приложения, вы на этом этапе можете также записать идентификатор пользователя.


Метод doGet, показанный в листинге 7.5, отыскивает объект document, соответствующий значению переменной qname, используя путь, указанный в файле свойств, и служебную библиотеку DOMlibrary. Если ему удается найти этот объект, далее он получает объект HttpSession и присоединяет к сеансу новый объект Interpreter, в котором хранится документ. Новый объект Recorder также инициализируется и присоединяется к сеансу. Для обработки любой ошибки и создания соответствующего сообщения используется метод errorMsg (см. листинг 7.7).

Объект Interpreter отвечает за создание форм HTML, с помощью которых будет осуществляться опрос, а объект Recorder отвечает за запись ответов пользователя. Эти классы обсуждаются в разделах «Класс Interpreter» и «Класс Recorder».

Результатом вызова метода doGet является страница с текстом, содержащимся в теге Intro документа XML. Для отображения этого текста на странице используется метод dolntro объекта Interpreter; также на странице располагается форма с кнопкой, при щелчке на которой появляется первый вопрос анкеты.



Листинг 7.5. Код метода doGet (QuestionnaireServ.java)

public void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String qname = req.getParameter("qname") ; // System.out.println("Start doGet"); if( qname == null || qname.length() == 0 ){ errorMsg( out, "Bad QNAME data", null); return; } // MUST have qname = name of xml file String src = qProp.getProperty( qname ); if( src == null ) { errorMsg( out, "Bad QNAME lookup", null ); return ; } String userid = "unknown" ; // customer or student id or unknown String tmp = req.getParameter("userid"); if( tmp != null ) userid = tmp; String usertype = "unknown" ; // "student" "customer" etc etc tmp = req.getParameter("usertype"); if(tmp != null ) usertype = tmp ; DOMlibrary lib = DOMlibrary.getLibrary(); System.out.println("DOMlibrary initialized, try for " + src ); Document doc = lib.getDOM( src ); if( doc == null ){ errorMsg( out, "DOM doc failed - unable to continue", null ); return ; } HttpSession session = req.getSession( true ); // if not new must be re-entering - could recover here Interpreter terpret = new Interpreter( doc, handler ); session.putValue( "xmldocument", terpret ); // session.setAttribute("xmldocument",terpret ); // the putValue method was used in the 2.1 API but is now // a deprecated method, // Recorder rb = new Recorder(userid, usertype, session.getId(), src ); rb.setMethods( doc ); session.putValue("recorder", rb ); //session.setAttribute("recorder", rb ); try { // terpret.doIntro( out ); // includes head and Form footer( out ); }catch(Exception e){ errorMsg( out, "doGet ", e ); }



После введения в анкету, как видно из листинга 7.6, все запросы и ответы проходят через метод doPost. После извлечения из объекта HttpSession объектов Interpreter и Recorder данные запроса собираются и с помощью объектов Interpreter и Recorder генерируется ответ.



Листинг 7.6. Метод doPost класса QuestionnaireServ (QuestionnaireServ.java)

public void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); //System.out.println("Start doPost"); HttpSession session = req.getSession(false); try { if( session == null ){ errorMsg(out, "No Session ", null ); return ; } Interpreter terpret = (Interpreter)session.getValue("xmldocument"); //deprecated //in 2.2 API we use this // (Interpreter)session.getAttribute("xmldocument"); Recorder rb = (Recorder) session.getValue("recorder"); // (Recorder) session.getValue("recorder"); if( terpret == null || rb == null ){ errorMsg( out, "Data not recovered from Session", null ); return; } terpret.doPostQ( out, req, rb ); footer( out ); }catch(Exception e ){ errorMsg( out, "doPost ", e ); } }

В листинге 7.7 показаны некоторые полезные вспомогательные методы класса QuestionnaireServ. Используя метод footer для размещения закрывающих те- гов на каждой странице, можно внизу указывать версию сервлета. Это очень удобно на этапе разработки, а впоследствии из финальной версии сервлета эти теги можно легко удалить.

Отладка сервлетов может оказаться очень непростым делом, особенно если ошибки проявляются лишь время от времени. Отладка особенно затрудняется в том случае, если пользователи не могут четко и однозначно выразить свои проблемы. Для сообщения об ошибках мы предпочитаем использовать метод erroMsg. Если ошибка связана с каким-либо исключением, то на HTML-странице появится сообщение с содержимым стека. Там же указывается и электронный адрес, по которому пользователь может послать сообщение об ошибке и, мы надеемся, копию содержимого стека. Разумеется, вам следует заменить приведенный ниже электронный адрес на свой.





Листинг 7.7. Вспомогательные методы сервлета QuestionnaireServ (QuestionnaireServ.java)

public void footer( PrintWriter out ){ out.println("<hr> Servlet version: " + version + "<br>"); out.println("</body>"); out.println("</html>"); out.close(); }

// assumes response has been set to text/html private void errorMsg ( PrintWriter out, String msg, Exception ex ){ out.println("<html>"); out.println("<head> <title>QuestionnaireServ Output</title></head>"); out.println("<body>"); out.println("<h2>Error: " ); out.println( msg ); out.println("</h2><br>"); if( ex != null ){ ex.printStackTrace( out ); } out.println("<br>"); out.println("<a href=\"mailto:wbrogden@bga.com\"> Please mail me the error message.</a><br>"); footer( out ); }

public String getServletInfo() { return "Administers a questionnaire"; }

}






Обеспечение конфиденциальности


В настоящее время многие разделяют беспокойство по поводу сохранения права потребителей на частную жизнь и с подозрением воспринимают любую организацию, которая хранит информацию в компьютерных базах данных. С одной стороны, все фирмы убеждены, что только максимально полная информация о потенциальных покупателях является ключом к победе в жестокой конкурентной борьбе. С другой стороны, люди испытывают тревогу при мысли, что подробности их частной жизни могут стать известными. Вы можете помочь своим клиентам чувствовать себя более уверенно при посещении вашего сайта, если доступно объясните те правила соблюдения конфиденциальности, которым вы следуете.

 



Обработка элемента Terminal


Как показано в листинге 7.18, метод doTerminal выполняет две основные задачи. Во-первых, он формирует завершающую страницу опроса, используя либо текст, содержащийся в элементе Terminal, либо какую-нибудь стандартную фразу. Кроме того, он пробует найти атрибут altfile тега Terminal. Если этот атрибут указан, то его значение (путь доступа к файлу) используется методом Recorder для сохранения результатов опроса в заданном файле; в противном случае метод Recorder по умолчанию выполняет запись в файл, который задается в начале документа XML.

Листинг 7.18. Метод genTerminal (Interpreter.Java)

// have reached the end of a terminal block // note that a <Terminal> tag may have an altfile="filepathandname" // that replaces the default established in the file attribute of // the <Questionnaire> tag for this particular branch private void genTerminal( PrintWriter out, Recorder recordB ){ NodeList nl = ((Element)nowBlock).getElementsByTagName ("Terminal"); int ct = nl.getLength(); String altfile = "" ; writeHead( out ); if( ct == 0 ){ out.println("Thank you for participating.<br>"); } else { // use text from <Terminal>...</Terminal> Element E = (Element)nl.item(0); // only one <Terminal> tag out.println( E.getFirstChild().getNodeValue() ); altfile = E.getAttribute("altfile"); } try { recordB.terminal( altfile ); }catch(IOException e ){ out.println("Problem recording results, please notify webmaster"); } }

 



Общественные организации


Многие считают, что частная информация подвергается такой же опасности при сборе данных, проводимых правительственными организациями, как и при опросах коммерческих организаций. Поэтому неудивительно, что существует несколько общественных организаций, деятельность которых связана с этой проблемой. Одной из таких организаций является EPIC (Electronic Privacy Information Center — Центр конфиденциальности электронной информации), расположенная в Вашингтоне, штат Колумбия (www.epic.org/privacy).

Организация EPIC занимается гражданскими правами и правами на частную жизнь. Часто она дает свидетельские показания на слушаниях дел в судах и, используя Акт о свободе информации (Freedom of Information Act), расследует и предает гласности нарушения правительственными организациями США прав граждан на частную жизнь.

Организация EPIC связана с Privacy International (www.privacy.org/pi) — международной коалицией групп, работающих в области защиты прав потребителей на конфиденциальность общения. Эта организация, основанная в 1990 году, базируется в Лондоне; она проводит международные конференции по проблемам соблюдения конфиденциальности. Каждый год она публикует список правительственных и коммерческих организаций, получивших титул «Big Brother» [Большой Брат. — Примеч. перев. ]. Этот титул Privacy International присваивает тем организациям, которые, по ее мнению, ведут наиболее агрессивную и нечестную политику в отношении использования частной информации.

Знание потребностей и интересов своих покупателей существенно для успеха любого предприятия. Но для того, чтобы у посетителей вашего сайта не возникло никаких опасений за их персональную информацию, лучше всего ясно изложить им принципы вашей политики в этом отношении и всегда предоставлять им возможность отказаться от сообщения несущественной информации. Если ваш сайт ориентирован на детей, вам также следует убедиться, что принципы работы с персональной информацией соответствуют принятому законодательству.

 



Определение последовательности вопросов


На рис. 7.1 показаны возможные ветви сценария опроса. Существенно следующее: имеются блоки вопросов, в которых ветвление отсутствует и которые заканчиваются многовариантным вопросом — ответ на такой вопрос ведет к переходу по той или иной ветви. Многовариантный вопрос может приводить несколько ветвей к одному блоку (например, блоки С и D приводят к одному блоку Е) или к различным блокам (например, из блока А можно попасть, в зависимости от ответа на многовариантный вопрос, в блок В, С или D). Блоки либо заканчиваются многовариантными вопросами, либо являются завершающими и прекращают опрос. По достижении завершающего блока система записывает все ответы в файл. Система может быть устроена таким образом, чтобы каждому завершающему блоку соответствовал свой уникальный файл.

Переводя эту диаграмму на язык сущностей XML, мы приходим к следующей структуре. В документе Questionnaire на первом уровне имеется элемент Intro (введение) и одна или несколько сущностей Block. Каждая сущность Block содержит одну или более сущностей Ques (вопросы) и может заканчиваться сущностью Terminal. В сущности Block имеется атрибут name, который используется для адресации ветвления, и атрибут type, значение которого равно terminal, если блок заканчивается тегом Terminal. Этот внешний уровень структуры системы опросов схематически описан в листинге 7.1 [Questionnaire — анкета, intro — введение (сокращение от introduction). — Примеч. перев. ].

Листинг 7.1. Первый и второй уровень иерархии анкеты

<Questionnai re> <Intro> </Intro>

<Block name="A"> </Block>

<Block name="B" type="terminal"> </Block>

<Block name="C"> </Block>

<Block name="D"> </Block>

<Block name="E" type="terminal"> </Block>

</Questionnai re>

Рис. 7.1. Возможные ветви сценария

Каждый вопрос в блоке создается с помощью тега dues, который включает в себя текстовый фрагмент и два или более варианта выбора, создаваемых тегами Qopt. Атрибуты тегов dues и Qopt позволяют контролировать ветвление и способ представления вопросов. Самый простой способ увидеть, как все это устроено, — рассмотреть несколько вопросов, представленных в примере анкеты в следующем разделе.

 



Отображение вопросов


Метод genQues, приведенный в листинге 7.12, вызывается после того, как переменной nowNode присваивается значение того элемента dues, который требуется отобразить. Обратите внимание на то, как используется указатель типа вопроса (QMC или QMCM) для выбора сообщения-подсказки. После того как напечатаны текст вопроса и подсказка, метод genQues создает форму, содержащую различные варианты ответа.

Листинг 7.12. Метод genQuest (Interpreter-Java)

public void genQuest( PrintWriter out ){ Element E = (Element) nowNode ; String qid = E.getAttribute("id") ; String type = E.getAttribute("type"); String lim = E.getAttribute("limit"); // out.print("Question id: " + qid + " type: " + type + brcrlf ); writeHead( out ); NodeList nm = E.getElementsByTagName("Qtext"); out.print( nm.item(0).getFirstChild().getNodeValue() ); out.println(brcrlf ); NodeList opm = E.getElementsByTagName("Qopt"); int optCt = opm.getLength(); int typeN = lookUpType( type ); switch( typeN ){ case QMC : out.print("Choose one"); break ; case QMCM : if( lim.length() == 0 ){ out.print("Choose any number"); } else { out.print("Choose up to " + lim ); } break ; default : out.print("Unknown type"); } out.print( brcrlf ); startForm( out ); // creates <form... for( int i = 0 ; i < optCt ; i++ ){ doOption(out, opm.item(i), typeN ); } endForm( out, qid ); }

Метод genQuest в предыдущем листинге вызывает метод doOption (листинг 7.13) для каждого элемента <Qopt>. Если вы захотите добавить дополнительные типы ответов, например поле для ввода текста, вам потребуется модифицировать именно этот метод. В этом листинге также показан метод checkBl ockType, который используется для проверки атрибутов элемента Block.

Листинг 7.13. Метод doOption (Interpretr.java)

// opN is from node list of <Qopt> - create output // <Qopt val="a" branch="" >Option a.</Qopt> private void doOption(PrintWriter out, Node opN, int typeN ){ Element E = (Element) opN; String val = E.getAttribute("val") ; String branch = E.getAttribute("branch"); String content = E.getFirstChild().getNodeValue(); // what else? type of option display? switch( typeN ){ // known valid case QMC : out.print("<input name=\"opt\" value=\"" + val + "\" type=\"RADIO\" >" ); break ; case QMCM : out.print("<input name=\"opt\" value=\"" + val + "\" type=\"CHECKBOX\" >" ); break ; } // now for the text out.println( content ); out.println( brcrlf ); }

// look at the type and css attributes in <Block> private void checkBlockType( ){ Element E = (Element)nowBlock ; String tmp = E.getAttribute("type"); terminal = tmp.equals("terminal"); tmp = E.getAttribute("css"); if( tmp.length() > 0 ) css = tmp ; System.out.println("checkBlockType - css:" + css ); }



Отображение введения


В листинге 7.14 представлен метод dolntro, который задает начальный элемент Block, отыскивая первый элемент в списке узлов ЫockNodeList. Когда значение переменной nowBlock задано, вызывается метод setQNodelnBlock для задания значения переменной nowNode равным первому элементу Ques. Если все это успешно выполнено, отображается текст введения и простая форма, которая вызывает первый вопрос.

Листинг 7.14. Метод dolntro, который выводит текст из тега Intro (Interpreter.java)

// <head> has been set, we are in <body> public void doIntro(PrintWriter out ){ writeHead( out ); nowBlock = blockNodeList.item(0); if( nowBlock == null ){ out.println("Error 1 setting up first question.<br>"); return ; } if( setQnodeInBlock( 0 )== null ){ out.println("Error 2 setting up first question.<br>"); return ; } checkBlockType( ); // sets the terminal flag out.println( getIntro() ); out.print("<form method=\"POST\" action=\"" ); out.print( actionStr ); out.println("\" >"); endForm( out, "intro" ); }

 



Пример анкеты


В листинге 7.2 показаны введение и первый вопрос анкеты, которую вы могли бы использовать для определения, какие товары добавить в ваш каталог.

ПРИМЕЧАНИЕ

Полная версия анкеты имеется на прилагаемом к книге компакт-диске. Тип первого вопроса (атрибут type) определен как QMC (Question Multiple Choice — вопрос, допускающий выбор одного ответа из списка); это вопрос, в качестве ответа на который пользователь должен выбрать один из предложенных вариантов.

Листинг 7.2. Начало документа XML, определяющего анкету (customsurvey.xml)

<?xml version="1.0" standalone="yes" ?> <Questionnaire title="Example Customer Survey" author="wbb" date="May 30, 2000" method="xml" file="e:\scripts\questionnaire\surveyresult.xml" > <Intro><![CDATA[ <h1>Welcome Customers</h1><br> <p>We here at <i>BuyStuff.com</i> want to meet your every desire to buy <b>STUFF</b>. To that end we are greatly expanding our on-line catalog and we want to concentrate on <b>STUFF</b> you will want to buy as soon as you see it. Please help by completing this simple survey. </p>]]> </Intro> <Block name="intro" type="terminal" > <Ques id="intro:1" type="QMC" > <Qtext>Which of the following are you most interested in buying on-line? </Qtext> <Qopt val="a" branch="books" >Books</Qopt> <Qopt val="b" branch="cds" >Cds</Qopt> <Qopt val="c" branch="gadgets">Electronic goodies</Qopt> <Qopt val="d" >I am not interested in buying Stuff!</Qopt> </Ques> <!-- this terminates the block and the questionnaire - could substitute different file for recording --> <Terminal><![CDATA[<h2>Thanks for looking anyway!</h2> ]]> </Terminal> </Block>


Элементы Qopt в первом вопросе со значениями атрибута val, равными a, b и с, служат ответвлениями к другим блокам, в то время как вариант ответа d приводит к отображению завершающего сообщения. Можно выбрать только один из предложенных ответов, потому что тип вопроса указан как QMC. В этой главе мы используем вопросы только двух типов: QMC (Question Multiple Choice — вопрос, допускающий выбор одного ответа из списка) и QMCM (Question Multiple Choice Multiple Answer — вопрос, допускающий выбор нескольких ответов из списка). Результаты ответа пользователя на этот вопрос записываются с помощью атрибута id вопроса и значений атрибутов val элементов Qopt.
В листинге 7.3 показан блок (из листинга 7.2), на который указывает атрибут branch тега Qopt, причем в последнем атрибут val имеет значение а. Вопрос с идентификатором books:! относится к типу QMCM, то есть позволяет выбрать несколько вариантов ответа. Блоки cds и gadgets устроены похожим образом.

Листинг 7.3. Блок вопросов «Книги» (custom.ersurvey.xml)
<Block name="books" type="terminal" > <Ques id="books:1" type="QMCM" > <Qtext>Please select all of the book categories you would like to see in our catalog. </Qtext> <Qopt val="0">Best Sellers of all types</Qopt> <Qopt val="1">Science Fiction</Qopt> <Qopt val="2">Fantasy Fiction</Qopt> <Qopt val="3">History and Biography</Qopt> <Qopt val="4">Computer Technology</Qopt> <Qopt val="5">Business Related</Qopt> </Ques> <Terminal><![CDATA[<h2>Thanks for answering the survey! </h2>]]> </Terminal> </Block>



Пример сервлета для просмотра результатов отчета


Сервлет QanalysisServ, рассматриваемый в этом разделе, предоставляет доступ по сети к снимку результатов проходящего опроса, поэтому его можно назвать сер- влетом для просмотра результатов отчета. Он использует файл questionnaire.pro- perties для поиска всех текущих опросов и предлагает вам выбрать один из них. Затем он определяет, какие выходные файлы генерируются в этом опросе, и предлагает выбрать один из них для формирования отчета.

В листинге 7.31 показаны инструкции импорта, статические переменные и метод init — начало кода сервлета.

Листинг 7.31. Сервлет QanalysisServ (QanalysisServ.java)

package com.XmlEcomBook.Chap07;

import com.XmlEcomBook.DOMlibrary ; import org.w3c.dom.* ; import com.sun.xml.tree.* ; import java.io.*; import java.util.* ; import javax.servlet.*; import javax.servlet.http.*;

public class QanalysisServ extends HttpServlet { static String brcrlf = "<br>\r\n" ; static String homedir = "e:\\scripts\\questionnaire" ; static String handler = "http://www.lanw.com/servlet/Qanalysis" ; static String passwd = "lovexml" ; static String version = "v1.0 May 28"; Properties qProp ;

// note we share properties file with QuestionnaireServ public void init(ServletConfig config) throws ServletException { super.init(config); System.out.println("Start QanalysisServ "); homedir = config.getInitParameter("homedir") ; File f = new File( homedir, "questionnaire.properties"); try { qProp = new Properties(); qProp.load( new FileInputStream(f) ); String tmp = qProp.getProperty("analysis"); if( tmp != null ) handler = tmp ; System.out.println("Loaded properties for Qanalysis: " + handler ); }catch(IOException e){ System.out.println("QanalysisServ Error loading " + e ); } }

Входом в сервлет служит метод doGet, показанный в листинге 7.32. Обычно для входа используется форма, в которую требуется ввести пароль пользователя, просто чтобы избежать риска случайного доступа к сервлету постороннего пользователя. Предположим, что все прошло успешно; тогда этот метод генерирует страницу с формой, которая позволяет осуществить выбор среди всех имеющихся в наличии отчетов. Выбранный файл передается методу doPost.



Листинг 7.32. Метод doGet класса QanalysisServ (QanalysisServ.java)
// entry with password public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("Qanalysis doGet"); resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String user = req.getParameter("username"); String tmp = req.getParameter("userpw"); // Obviously this could be a lot more complex if( !passwd.equals( tmp )){ errorMsg( out, "404 page not found", null ); return ; } if( qProp == null || qProp.size() == 0 ){ errorMsg( out, "Bad Initialization", null ); return ; } HttpSession session = req.getSession( true ); session.putValue( "username", user ); // Enumeration e = qProp.keys(); Vector v = new Vector(); while( e.hasMoreElements()){ String key = (String)e.nextElement(); // everything not "handler" or "analysis" is a XML file path name if( !( key.equals("handler") || key.equals("analysis"))){ v.addElement( key ); } } if( v.size() == 0 ){ errorMsg( out, "No Questionnaire files found", null ); return ; } out.println("<HTML>"); out.println("<HEAD><TITLE>QanalysisServ Output</TITLE> </HEAD>"); out.println("<BODY>"); out.println("<h2>Select The Questionnaire XML File</h2>"); out.println("Found " + v.size() + " XML files" + brcrlf ); out.println("<form method=\"POST\" action=\"http://localhost/servlet/Qanalysis\" >"); out.println("<select name=\"source\" >"); for( int i = 0 ; i < v.size() ; i++){ tmp = (String) v.elementAt( i ); out.println("<option value=\"" + tmp + "\" >" + tmp ); } out.println("</select>"); out.println("<input type=\"hidden\" name=\"username\" value=\"" + user + "\"><br>" ); out.println("<input type=\"hidden\" name=\"action\" value=\"select\" ><br>"); out.println("<input type=\"submit\" value=\"Start\" ><br>" ); out.println("</form>"); footer( out ); }


В первом запросе, выполняемом методом POST, значение переменной action равно select. Как показано в листинге 7.33, такой запрос инициирует вывод раскрывающегося списка всех доступных файлов с результатами. При этом используется метод createQList.

Листинг 7.33. Первая часть метода doPost (QanalysisServ.java)
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String source = req.getParameter( "source"); String action = req.getParameter( "action"); String ansfile = req.getParameter("ansfile"); // select when choosing quesionnaire XML file // analyze when choosing reformatted result file String[] files = null ; if( action == null || source == null || source.length() == 0 ){ errorMsg(out,"Bad source selection", null );return ; } // source is short name from properties String srcfile = qProp.getProperty( source ); if( srcfile == null ) { errorMsg( out, "Bad Source lookup", null ); return ; } HttpSession session = req.getSession(false); try { if( session == null ){ errorMsg(out, "No Session ", null ); return ; } DOMlibrary lib = DOMlibrary.getLibrary(); System.out.println("DOMlibrary ok, try for " + srcfile ); Document doc = lib.getDOM( srcfile ); if( doc == null ){ errorMsg( out, "DOM doc failed - unable to continue", null ); return ; } PrepQxml pQ = (PrepQxml)session.getValue("prepqxml"); // substitute getAttribute if using 2.2 api header( out ); if( pQ == null ){ // first pass pQ = new PrepQxml( doc ); files = pQ.createFiles(); session.putValue("prepqxml",pQ); } else { files = pQ.getFiles(); } if( action.equals("select") ){ out.println("<h1>Test: " + pQ.title +"</h1>" ); out.println("<p>XML questionnaire file: <i>" + source + "</i></p>"); out.println("<p>Author: " + pQ.author + " Dated: " + pQ.date + "</p>"); out.println("<p>The primary answer file is: " + pQ.primaryfile + "</p>" ); out.println("<p>There " ); if( files.length < 2 ) out.println("are no other "); if( files.length == 2 ) out.println("is one other "); if( files.length > 2 ) out.println( (files.length - 1) + " other "); out.println("answer file(s). Select a file and click <b>Start</b></p>"); createQList( out, source, files ); }


Когда пользователь выберет один из файлов с результатами, переменная action принимает значение analyze. Как показано в листинге 7.34, это инициирует создание нового объекта TallyQues, который используется для формирования таблицы с результатами.

Листинг 7.34. Метод doPost, продолжение (QanalysisServ.java)
if( action.equals("analyze") ){ out.println("<h1>Analysis</h1>"); out.println("<p>XML questionnaire file: <i>" + source + "</i></p>"); String ansXml = pQ.getAnsXml( ansfile ); out.println("<p>Answer file: " + ansfile + "</p>"); out.println("<p>Processing file: " + ansXml + "</p>"); TallyQues tQ = new TallyQues( doc ); // build with questions if( tQ.tallyAns( ansXml )== null ){ out.println("<h2>Error " + tQ.lastErr + "</h2>") ; } else { tQ.formatTally( out ); } } footer( out ); }catch( Exception ex ){ errorMsg( out, "QanalysisServ.doPost ", ex ); } }
Метод createQList, показанный в листинге 7.35, создает форму HTML, которая используется для вывода всех возможных файлов с ответами.

Листинг 7.35. Метод createList (QanalysisServ.java)
// the PrepQxml has located all of the answer files - only one // can be analyzed at at time void createQList( PrintWriter out, String source, String[] files ){ out.println("<form method=\"POST\" action= \"http://localhost/servlet/Qanalysis\" >"); out.println("<input type=\"hidden\" name= \"action\" value=\"analyze\" ><br>"); out.println("<input type=\"hidden\" name= \"source\" value=\"" + source + "\" ><br>"); out.println("<select name=\"ansfile\" >"); for( int i = 0 ; i < files.length ; i++){ String tmp = files[i]; out.println("<option value=\"" + tmp + "\" >" + tmp ); } out.println("</select>"); out.println("<input type=\"submit\" value=\"Start\" ><br>" ); out.println("</form><br>"); }


Мы почти завершили рассмотрение класса QanalysisServ! В листинге 7.36 показаны некоторые служебные методы, необходимые для форматирования выходных страниц и сообщений об ошибках.

Листинг 7.36. Служебные методы в классе QanalysisServ (QanalysisServ.java)
public void header( PrintWriter out ){ out.println("<HTML>"); out.println("<HEAD><TITLE>QanalysisServ Output</TITLE> </HEAD>"); out.println("<BODY>"); } public void footer( PrintWriter out ){ out.println("<hr>" + version + "<br>"); out.println("</BODY>"); out.println("</HTML>"); out.close(); } // assumes response has been set to text/html private void errorMsg ( PrintWriter out, String msg, Exception ex ){ out.println("<html>"); out.println("<head><title>QanalysisServ Output</title> </head>"); out.println("<body>"); out.println("<h2>" ); out.println( msg ); out.println("</h2><br>"); if( ex != null ){ ex.printStackTrace( out ); } out.println("<br>"); footer( out ); } }
 


Промышленные стандарты


Желание решить описанную проблему в области электронной коммерции без вмешательства правительства привело к созданию независимой некоммерческой инициативной организации под названием TRUSTe [Trust (англ.) — доверие. — Примеч. перев. ] (www.truste.org). Основными принципами TRUSTe являются следующие.

Пользователь имеет право на осведомленность и осознанный выбор того, каким образом будет использоваться его персональная информация.

Ни один из принципов соблюдения конфиденциальности не годится для всех случаев.

Организация TRUSTe осуществляет проверку соблюдения принципов конфиденциальности, так же как, например, организация UL (United Laboratories — объединенные лаборатории) проводит проверки в области электротехнического оборудования- Организации — члены программы получают право на размещение на своих сайтах логотипа TRUSRe, если принципы обеспечения конфиденциальности, которым они следуют, удовлетворяют стандартам TRUSTe. Также эти организации должны заплатить членский взнос и пройти контрольную проверку. Членский взнос составляет сумму 299 долларов для компаний, чей годовой доход не превышает одного миллиона долларов, так что членство в программе TRUSTe вполне доступно.

ПРИМЕЧАНИЕ

Сайты, которые могут собирать информацию у детей в возрасте до 13 лет, должны удовлетворять еще более жестким требованиям в отношении соблюдения конфиденциальности. В настоящее время Конгресс США готовит новое законодательство в данной области.

TRUSTe постоянно проводит наблюдения за теми сайтами, которые являются членами программы, и проверяет соблюдение ими заявленной практики обеспечения конфиденциальности. В процессе наблюдения, в частности, на сайт отправляется вымышленная персональная информация, а затем отслеживается ее использование. Кроме того, TRUSTe активно преследует те сайты, которые несанкционированно размещают логотип TRUSTe.

Как для членов программы, так и для других организаций сайт www.truste.org представляет собой прекрасное место для знакомства с новостями в области охраны права пользователей на конфиденциальность, особенно в США. TRUSTe также сотрудничает со многими другими организациями, которые пытаются установить стандарты в сфере охраны частной информации пользователей.



Сервлет управления опросом


В этом разделе приводится полный код сервлета Java, который управляет опросом, основанным на приведенном в предыдущем разделе документе XML. Этот сервлет записывает результаты в форме, удобной для дальнейшего анализа. Также мы опишем служебный класс, который позволяет управлять объектами document в процессоре сервлетов, и сервлет для составления таблицы и отчета по результатам опроса.

 



Служебная библиотека документа XML


Существует альтернативный подход к тому, чтобы обеспечить каждому сервлету доступ к соответствующему документу XML. Этот подход заключается в использовании служебных библиотек. В этом случае сервлет просто запрашивает документ из библиотеки независимо от того, находится ли файл XML на диске и где именно или он уже вызван в результате какого-либо другого запроса и находится в памяти.

Ниже перечислены характеристики созданного нами класса DOMlibrary.

Построен по шаблону единичного класса (singleton), который допускает создание только одного экземпляра класса. В такой схеме отсутствует открытый (public) конструктор, вместо него имеется статический метод, контролирующий создание единичного экземпляра класса и доступ к этому экземпляру.

Когда поступает запрос на документ XML, экземпляр DOMlibrary проверяет время создания файла, содержащего документ. Это гарантирует, что в ответ на запрос не будет выдан устаревший документ.

Реализует интерфейс Runnable, поэтому в нем может содержаться объект Thread, который периодически выполняет некоторые вспомогательные функции. Типичной вспомогательной функцией является исключение из памяти объектов Document, которые давно не использовались. Таким образом, постоянно задействованные документы, как правило, окажутся в памяти, в то время как редко используемые документы не будут занимать память, истощая ее ресурсы.

Вместо того чтобы использовать единичный класс, можно было бы реализовать все только через статические методы. Однако благодаря шаблону единичного класса мы выигрываем в отношении гибкости, получая, в частности, возможность реализовать интерфейс Runnabl e и использовать метод run для управления жизненным циклом документа в памяти. Шаблон единичного класса очень часто можно встретить в стандартной библиотеке Java.

В листинге 7.37 показан статический метод getLibrary, который при необходимости создает новый объект DOMlibrary. Все сервлеты, которым требуется доступ к документу XML, вызывают метод getLibrary для получения ссылки на единственный экземпляр библиотеки, а затем с помощью этой ссылки запрашивают нужный документ. Переменная maxAge используется в методе run для того, чтобы определить, когда документ следует убрать из памяти.




Листинг 7.37. Инструкции импорта и статические методы класса DOMlibrary (DOMIibrary.java)

package com.XmlEcomBook ;

import java.io.* ; import java.util.* ; import com.sun.xml.tree.* ; import com.sun.xml.parser.Resolver ; import org.xml.sax.* ; import org.w3c.dom.* ;

public class DOMlibrary implements java.lang.Runnable { private static DOMlibrary theLib ; private static int maxAge = 6000 ; // age in seconds public synchronized static DOMlibrary getLibrary(){ if( theLib == null ) theLib = new DOMlibrary(); return theLib ; } public static void setMaxAge(int t) { maxAge = t ;}

Как показано в листинге 7.38, единственный конструктор является закрытым (private), чтобы гарантировать, что только лишь статический метод getLibrary может создать новый объект. Резидентные объекты XML document хранятся в хэш- таблице domHash; ключом является путь к соответствующему файлу. Хэш-табли- ца с именем trackerHash, используя тот же ключ, сохраняет объект DomTracker для каждого объекта XML document. Класс DOMTracker — внутренний класс в DOMlibrary; его код приведен в листинге 7.43. Обратите внимание на то, что объекту Thread (потоку, выполняющему метод run), присвоен самый низкий приоритет.



Листинг 7.38. Конструктор и переменные экземпляра класса DOMlibrary (DOMIibrary.java)

private Hashtable domHash, trackerHash ; boolean running ; private String lastErr = "none" ; // private constructor to ensure singleton private DOMlibrary(){ domHash = new Hashtable(); trackerHash = new Hashtable(); Thread upkeep = new Thread(this,"DOMlibrary upkeep"); upkeep.setPriority( Thread.MIN_PRIORITY ); running = true ; upkeep.start(); }

Анализ документа XML в DOMlibrary осуществляется в методе loadXML, как показано в листинге 7.39. Чтобы избежать многократных попыток загрузить документ с неверно указанным атрибутом scr (путь к файлу) или документ, загрузка которого вызывает синтаксическую ошибку, этот метод помещает в таблицу domHash строку, содержащую сообщение об ошибке, если таковая встречается. Если анализ документа проходит успешно, в таблицу trackerHash записывается соответствующий объект DomTracker. Это единственный метод, в котором вызываются специфические для анализа документов методы; если бы вы вместо анализатора Sun использовали для анализа что-либо другое, вам потребовалось бы несколько модифицировать этот метод.





Листинг 7.39. Метод loadXML осуществляет анализ документа XML (DOMIibrary.java)

private synchronized void loadXML(File xmlFile, String src ) { //File xmlFile = new File( src ) ; try { long timestamp = xmlFile.lastModified(); InputSource input = Resolver.createInputSource( xmlFile ); // ... the "false" flag says not to validate (faster) // XmlDocument is in the com.sun.xml.tree package Document doc = XmlDocument.createXmlDocument (input, false); domHash.put( src, doc ); trackerHash.put( src, new DomTracker( timestamp ) ); }catch(SAXParseException spe ){ StringBuffer sb = new StringBuffer( spe.toString() ); sb.append("\n Line number: " + spe.getLineNumber()); sb.append("\nColumn number: " + spe.getColumnNumber() ); sb.append("\n Public ID: " + spe.getPublicId() ); sb.append("\n System ID: " + spe.getSystemId() + "\n"); lastErr = sb.toString(); System.out.print( lastErr ); }catch( SAXException se ){ lastErr = se.toString(); System.out.println("loadXML threw " + lastErr ); domHash.put( src, lastErr ); se.printStackTrace( System.out ); }catch( IOException ie ){ lastErr = ie.toString(); System.out.println("loadXML threw " + lastErr + " trying to read " + src ); domHash.put( src, lastErr ); } } // end loadXML

Когда сервлетам требуется получить документ, они вызывают метод getDOM, показанный в листинге 7.40. Если при создании документа возникают какие-либо проблемы, то вместо ссылки на документ этот метод возвращает null. Каждый раз, когда в хэш-таблице обнаруживается требуемый документ, в ассоциированном объекте DomTracker обновляется значение времени последнего использования, которое заменяется текущим временем. Заметим, что в нескольких местах создаются выходные сообщения, которые записываются в объект System.out и служат для отладки. Мы советует закомментировать их после того, как система заработает.



Листинг 7.40. Метод getDOM (DOMIibrary.java)

// either return the doc or null if a problem public synchronized Document getDOM( String src ){ Object doc = domHash.get( src ); DomTracker dt = (DomTracker) trackerHash.get( src ); boolean newflag = false ; File f = null ; if( doc == null ){ System.out.println("DOMlibrary.getDOM new " + src ); f = new File( src ); loadXML( f, src ); // sets trackerHash doc = domHash.get( src ); dt = (DomTracker) trackerHash.get( src ); newflag = true ; System.out.println("DOMlibrary load OK"); } else { // found a document - is it up to date? f = new File( src ); if( dt.changed( f )){ System.out.println("DOMlibrary reloads " + src ); loadXML( f, src ); // sets trackerHash newflag = true ; doc = domHash.get( src ); dt = (DomTracker)trackerHash.get( src ); } } // if not a document, must be a string due to error if( ! (doc instanceof Document )){ System.out.println("DOMlibrary: " + doc ); // could try for re-read here } if( doc instanceof Document ) { if( ! newflag ){ dt = (DomTracker)trackerHash.get( src ); dt.setLastUse( System.currentTimeMillis()); } return (Document) doc ; } return null ; }



В листинге 7. 41 представлена пара служебных методов, которые используются для удаления документа из памяти или для его перезагрузки.



Листинг 7.41. Некоторые служебные методы (DOMIibrary.java)

// use this to force removal of a dom. it // returns last copy of dom or null if dom not in hash public synchronized Document removeDOM( String src ){ Document dom = (Document)domHash.get( src ); if( dom != null ){ domHash.remove( src ); trackerHash.remove( src ); // System.out.println("Removed " + src ); } return dom ; }

// call this to force a reload after src is modified public synchronized Document reloadDOM( String src ){ if( domHash.get( src ) != null ){ domHash.remove( src ); trackerHash.remove( src ); } return getDOM( src ); }

Класс DOMlibrary должен реализовывать интерфейс Runnable, чтобы можно было использовать в фоновом режиме поток, имеющий минимальный приоритет и выполняющий служебные функции. Пример, приведенный в листинге 7.42, очень прост: поток удаляет все документы, которые давно не используются. Также в листинге 7.42 показаны служебные методы toString и getLastErr.



Листинг 7.42. Метод run и другие служебные методы (DOMIibrary.java)

// run is used for upkeep, not reading XML public void run() { while( running ){ try{ Thread.sleep( 60000 ); // example management code Enumeration keys = trackerHash.keys(); long time = System.currentTimeMillis(); while( keys.hasMoreElements() ){ String key = (String) keys.nextElement(); if(((DomTracker)trackerHash.get(key)).getAge(time) > maxAge ){ removeDOM( key ); } } }catch(InterruptedException e){ } }// end while }

public String getLastErr(){ return lastErr ; }

public String toString() { StringBuffer sb = new StringBuffer("DOMlibrary contains "); int ct = domHash.size(); if( ct > 0 ){ sb.append(Integer.toString( ct ) ); sb.append( " DOM objects "); Enumeration e = domHash.keys(); while( e.hasMoreElements() ){ String key = (String)e.nextElement(); sb.append( key ); sb.append(" " ); } } else { sb.append("no DOM objects"); } sb.append(" Last error: " ); sb.append( lastErr ); return sb.toString(); }



Экземпляр внутреннего класса DOMTracker создается всякий раз, когда загружается документ XML. Этот экземпляр подвергается обработке параллельно с самим объектом document. В нынешней версии нас интересуют только два параметра: время, когда документ XML был создан, и время, когда последний раз поступал запрос на этот документ. Как показано в листинге 7.43, метод getAge возвращает время в секундах, прошедшее с момента последнего использования документа, а метод changed проверяет время создания или последней модификации исходного файла.



Листинг 7.43. Определение класса DOMTracker как члена класса DOMIibrary (DOMIibrary.java)

// utility class to aid in tracking memory resident DOM class DomTracker { private long lastMod ; private long lastUse ; DomTracker( long timestamp ){ lastMod = timestamp ; // from File.lastModified(); lastUse = System.currentTimeMillis(); } void setLastUse( long ts ){ lastUse = ts ; } int getAge( long now ){ // return value in seconds return (int)(( now - lastUse)/ 1000) ; } boolean changed( File f ){ long n = f.lastModified(); return !( n == lastMod ); } } }

Используя инструментальные средства, описанные в этой главе, вы можете создать гибкую систему получения информации о ваших покупателях — как потенциальных, так и уже покупающих товары в вашем магазине. Поскольку эта система использует сценарий, основанный на XML и допускающий возможность изменения без перезапуска сервера, вы можете направлять опрос в то или иное русло в зависимости от мнений, которые высказывает клиент по ходу опроса.


Создание системы опросов с помощью XML-сценария


Простая, но эффективная форма сбора информации — это проведение опроса по сети. Поскольку язык XML предназначен, в частности, для определения структуры документов, а опрос осуществляется как раз с помощью четко структурированных документов, XML как нельзя лучше подходит для решения нашей задачи. Основанная на XML система опросов, которой посвящены все остальные разделы этой главы, отличается гибкостью. В этой системе конкретный пользователь не связывается со своими ответами; система просто собирает ответы всех пользователей вместе. В систему несложно внести изменения, которые позволили бы сохранять ответы каждого пользователя в базе данных, но в таком случае вам пришлось бы объяснять пользователю, что именно вы собираетесь делать с его персональной информацией.

В этом разделе мы займемся разработкой обобщенной структуры XML для создания системы сетевых опросов. Мы начнем со списка критериев, которым должна отвечать система.

Управление внешним видом. В идеале, мы хотели бы, чтобы дизайн-страницы с вопросами не нарушал общего стиля нашего web-сайта.

Гибкость методов опроса. Мы должны иметь возможность применять различные методы опросов: от простых вопросов, на которые пользователь отвечает «да» или «нет», до выбора одного из предложенных ответов.

Возможность ветвления. В одном и том же сценарии очередность вопросов должна зависеть от ответов пользователей. Например, если из ответа на некоторый вопрос понятно, что данный пользователь никогда не покупает музыкальные компакт-диски через Интернет, не имеет смысла расспрашивать его о музыкальных предпочтениях и нужно выбрать другую ветвь вопросов.

Расширяемость. Если в систему потребуется добавить новую форму представления вопросов, необходимые изменения кода должны быть минимальными.

Запись результатов. Результаты опроса каждого участника должны быть записаны полностью и не должны зависеть от результатов других участников. Это дает максимальную гибкость при анализе.



Управление ветвлением опроса


В листинге 7.15 показан метод setBranch, который вызывается из метода doPostQ, когда ответ пользователя на данный вопрос определяет выбор очередной ветви опроса. Этот метод просто просматривает все элементы ВТ ock и ищет имя (значение атрибута name), совпадающее с заданным именем блока. В этом методе также устанавливаются значения переменных nowBlock и nowNode.

Листинг 7.15. Метод setBranch (Interpreter.java)

// jump to another block has been detected private void setBranch(String block ){ int ct = blockNodeList.getLength(); for( int i = 1 ; i < ct ; i++ ){ // block 0 was the start nowBlock = blockNodeList.item(i); String name = ((Element)nowBlock).getAttribute("name"); if( name.equals( block )){ checkBlockType() ; // to set terminal flag setQnodeInBlock( 0 ) ; // set nowNode return ; } } System.err.println("Interpreter.setBranch failed to find " + block ); nowBlock = nowNode = null ; }

Метод doPostQ, начало которого приводится в листинге 7.16, вызывается из метода doPost и управляет созданием новой HTML-страницы. Обратите внимание на то, что в первую очередь этот метод проверяет, не был ли зафиксирован соответствующим объектом Recorder тот факт, что опрос завершен. Это сделано для того, чтобы пользователь не мог с помощью кнопки Back (Назад) браузера возвратиться на предыдущую страницу для ввода данных, когда опрос уже завершен и данные записаны.

Следующая часть кода отводится для обработки специального случая, когда переменная quesid равна intro, то есть вопрос является первым в анкете. Во всех других случаях ответ пользователя записывается путем сравнения значений opt, взятых из формы, с атрибутами тега Qopt с помощью объекта Recorder этого сеанса.

Листинг 7.16. Начало метода doPost (Interpreter.java)

// req contains user response public void doPostQ( PrintWriter out, HttpServletRequest req, Recorder recordB ){ if( recordB.terminated ){ writeHead( out ); out.println("<b>This questionnaire has been terminated</b>"); return ; } String action = req.getParameter("action"); String quesid = req.getParameter("quesid"); if( !action.equals("Next") ){ out.println("Unexpected state in Interpreter.doPost<br>"); return ; } if( quesid.equals("intro") ){ // this calls for generating first question // doIntro already set nowNode to first <Ques> node genQuest( out ); return ; } // if here, not generating first question, examine request Element E = (Element) nowNode ; NodeList oplist = E.getElementsByTagName("Qopt"); int type = lookUpType( E.getAttribute("type")); String lim = E.getAttribute("limit"); // ? String[] optS = req.getParameterValues("opt"); recordB.record( quesid, type, optS );


Следующий шаг метода doPostQ, как показано в листинге 7.17, сводится к тому, чтобы определить, инициирует ли последний записанный ответ пользователя переход к новой ветви опроса. Естественно, новая ветвь начинается с первого вопроса в блоке, как указано в методе setBranch. Если переходить на новую ветвь не нужно, определяется место данного вопроса в текущем элементе Block и выполняется переход к следующему вопросу. Также нужно предусмотреть ситуацию, когда элемент, следующий за текущим вопросом, является завершающим; в этом случае мы вызываем метод getTerminal, функции которого описаны в следующем разделе и связаны с формированием последней страницы опроса.



Листинг 7.17. Метод doPost, продолжение (Interpreter.java)

String branch = branchLookUp( oplist, optS ); if( branch != null ){ //System.out.println("Taking Branch:" + branch ); setBranch( branch ); // sets nowBlock and nowNode to new value if( nowNode == null ) genTerminal( out, recordB ); else genQuest( out ); return ; }

// branch is null, nowBlock has 1 or more <Ques NodeList qlist = ((Element)nowBlock).getElementsByTagName("Ques"); int n = 0 ; int nct = qlist.getLength(); while( qlist.item(n) != nowNode && n < nct ) n++ ; // n = nowNode Node nxtN = qlist.item(n+1); if( nxtN != null ){ nowNode = nxtN ; genQuest( out ); System.out.println("Found nextQ"); return ; } if( terminal ) genTerminal( out, recordB ); else out.println("nextQ NULL, not terminal<br>" ); } // end doPostQ






Варианты анализа анкеты


Поскольку формат записи результатов опроса таков, что ответы каждого пользователя сохраняются в виде отдельной записи, имеется много вариантов анализа этих ответов. Для наших целей годится самый простой способ — составление таблицы по каждому вопросу, в которой для каждого варианта ответа указано количество выбравших его пользователей. Задействованные для составления таблиц классы отделены от сервлета, отвечающего за представление опроса в сети, и могут быть использованы для автономного создания HTML-страниц.

Первая проблема, которую предстоит решить, — это преобразование всех написанных сервлетом Questionnaire выходных файлов в формат, пригодный для анализа результатов опроса. Вспомним, что класс Recorder просто записывает теги <Qresults>, которые аккумулируются в выходном файле (или файлах). Нам нужно создать файл, в котором имелся бы корневой элемент. Фактически этот файл явился бы снимком (snapshot) собранных результатов опроса. Для этого программа, анализирующая результаты, должна выполнить следующие шаги.

Получить объект org.w3c.dom.Document, содержащий сценарий опроса.

Отыскать имена выходных файлов.

Создать для каждого новый файл, объединяя корневые элементы с текущим содержимым выходного файла.



Вспомогательные методы класса Interpreter


В листинге 7.19 показаны вспомогательные методы класса Interpreter.

Листинг 7.19. Завершение кода класса Interpreter (Interpreter.Java)

private Node setQnodeInBlock( int n ){ Element E = (Element) nowBlock ; NodeList nl = E.getElementsByTagName("Ques"); nowNode = nl.item( n ); return nowNode ; } //public String getTitle(){return title;} //public String getCSS(){return css ; } public String getIntro() { Element E = theDom.getDocumentElement(); // the root NodeList nl = E.getElementsByTagName("Intro"); Element I = (Element)nl.item(0); nl = I.getChildNodes(); int ct = nl.getLength(); if( ct == 0 ) return "Bad Intro Data<br>" ; return nl.item(0).getNodeValue(); }

// return String if any chosen opt has a branch="", else null private String branchLookUp( NodeList oplist,String[] optS ){ if( optS == null || optS.length == 0 ) return null ; Hashtable opHash = new Hashtable(); int i, ct = oplist.getLength(); String val, branch ; for( i = 0 ; i < ct ;i++ ){ val = ((Element)oplist.item(i)).getAttribute("val"); branch = ((Element)oplist.item(i)).getAttribute("branch"); opHash.put( val, branch ); // branch = "" if no attribute } if( opHash.size() == 0 ) return null ; // branch not possible for( i = 0 ; i < optS.length ; i++ ){ branch = (String)opHash.get( optS[i] ); if( branch != null && branch.length() > 0 ) return branch ; } return null ; }

public String toString() { StringBuffer sb = new StringBuffer("Interpreter "); return sb.toString() ; } }