Вы наверняка хотели бы, чтобы посетители вашего web-сайта могли легко находить интересующие их товары, добавлять их в корзину и производить оплату, в соответствии со своим интуитивным представлением об этих действиях как о неразрывно связанных этапах покупки товара. К сожалению, основные используемые в настоящее время HTTP- и web-протоколы не позволяют обеспечить эту неразрывность. В настоящей главе исследуется закулисная работа, необходимая для того, чтобы пользователи могли без затруднений приобретать товары в вашем электронном магазине.
Для посетителей виртуального магазина интуитивно понятна аналогия «корзины покупателя» (shopping cart). Поэтому им кажется, что все должно происходить так же, как и в реальном магазине, где корзина, в которую они складывают выбранные товары, остается с ними в течение всего времени посещения магазина, пока они не подошли к кассе и не заплатили за покупки. К сожалению, то, что посетителю виртуального магазина представляется как протяженный во времени единый процесс просматривания информации, для web-сервера является последовательностью не связанных друг с другом запросов, поступающих от браузера, и высылаемых ему ответов. Так получается потому, что основные HTTP- протоколы неустойчивы, то есть не позволяют определить, что очередной запрос должен быть связан с предыдущим, поскольку оба запроса поступают от одного пользователя.
Задача сохранения информации в течение одного сеанса (то есть в течение времени посещения данным пользователем данного сайта) обычно называется задачей сеансового мониторинга (session-tracking). Существуют три способа решения этой задачи: файлы cookie, скрытые поля (переменные) формы и перезапись URL.
Файл cookie — это фрагмент текстовой информации, который ассоциируется браузером с адресом конкретного web-сайта Web-сервер посылает данные (cookie) вместе с ответом, а web-браузер хранит полученный текст. Каждый раз, когда web-браузер посылает запрос этому же web-серверу, файл cookie присоединяется к одному из заголовков сообщения, а сервер отыскивает и интерпретирует cookie. Текст файла cookie имеет хорошо знакомую вам форму атрибут = значение, похожую на имена и значения атрибутов XML.
Очень полезное свойство файла cookie заключается в том, что программист имеет возможность контролировать время жизни этого фрагмента данных. Вы можете создать файл cookie, который будет существовать только в течение одного посещения сайта пользователем, в течение фиксированного промежутка времени или иметь неограниченный срок действия. Коммерческие web-сайты часто используют файлы cookie с длительным сроком действия для того, чтобы запоминать пользователей, часто посещающих данный сайт.
Интерфейс прикладных программ (API) сервлетов Java предоставляет средства для того, чтобы посылать и принимать данные файлы cookie, и тем самым решает многие задачи мониторинга сеанса. Тем не менее здесь имеются определенные ограничения, связанные с тем, что web-браузер лимитирует объем текста, содержащегося в файле cookie, и общее число таких файлов, связанных с одним сайтом. Поэтому хранение всей информации о корзине пользователя в виде файла cookie непрактично, так что обычно в файле cookie хранится только уникальный идентификатор, который присваивается данному пользователю и используется как ключ для поиска на сервере требуемой информации об этом пользователе.
При использовании скрытых полей формы для мониторинга сеанса ваша программа должна заносить скрытые данные в каждую форму или ссылку на каждой HTML-странице. Например, если нам нужно отследить значение переменной customer Id, в каждой форме должен содержаться следующий элемент:
<input type="hidden" name="customerid" value="124c41">
Кроме того, каждая ссылка должна включать в себя соответствующий фрагмент:
href= "http://Iocalhost/servlet/catalog?customerid=124c41"
Здесь символ ? разделяет URL и строку запроса.
Значение customer id затем извлекается в методе сервлета doPost или doGet с помощью кода, подобного следующему:
String customerid = req.getParameter("customerid")
В технологии, известной под названием перезаписи URL (rewriting URL), к каждому URL-адресу, идентифицирующему данный сеанс, добавляется дополнительная информация. Выбранный для этого формат не должен мешать web- серверу пересылать запрос на правильный сервлет, но сервлет должен иметь возможность обнаружить эту дополнительную информацию. В языке Java перезапись URL осуществляется с помощью точки с запятой (;). Таким образом, в результате URL выглядит так:
href= "http://Tocalhost/servlet/catalog;jsessiornd=124c41"
Для мониторинга сеанса в сервлете ShoppingCart мы используем интерфейс API класса HttpSession. Этот класс позволяет процессору сервлетов детально управлять мониторингом сеанса с минимальным участием программиста.
Использование объектов класса HttpSession
Процессор сервлетов Java (который иногда называют контейнером сервлетов) управляет коллекцией объектов, реализующих интерфейс HttpSession. Наша программа может использовать один из этих объектов для хранения любой информации, необходимой при создании корзины покупателя. Каждый объект HttpSession имеет идентификатор (id), представляющий собой строку, которая создается одновременно с объектом и гарантированно является уникальной. Этот уникальный идентификатор создается одним из двух возможных методов.
Мониторинг файлов cookie. Значение идентификатора отсылается на web- браузер пользователя как заголовок файла cookie, сгенерированного объектом HttpServl etResponse. Стандартное имя такого файла cookie — jsessionid, а значением может быть строка, подобная следующей: "97187996250188366", то есть просто случайное число, генерируемое процессором сервлетов. Это значение автоматически считывается при поступлении следующего запроса от того же пользователя, так что наша программа просто запрашивает объект HttpSession, принадлежащий данному пользователю.
Перезапись URL. Значение идентификатора присоединяется к каждому URL-адресу, который посетитель сайта может использовать для отправки запроса на сервер. Поскольку сюда входят также URL-адреса, которые являются частью статических шаблонов или динамически генерируемого текста, программист должен приложить дополнительные усилия, чтобы гарантировать, что все URL-адреса будут переписаны.
Чтобы не усложнять наш пример, в сервлете, реализующем корзину покупателя, мы будем использовать подход с мониторингом файлов cookie:
HttpSession session = req.getSession(true);
if( session.isNew() ){
System.out.print("Session is new " + session.getld() ); }
Этот простой фрагмент кода метода doPost формирует объект HttpSession для данного пользователя. Переменная req является объектом класса HttpServl etRequest. Булева постоянная true в вызове метода getSession указывает процессору сервлетов, что нужно создать новый объект сеанса, если в пользовательском запросе отсутствует соответствующий идентификатор. Вторая строка содержит метод isNew, позволяющий определить, является ли данный сеанс новым или клиент (браузер) уже участвовал в сеансе.
В интерфейсе API для JSP-страниц переменная класса HttpSession называется session, поэтому желательно и в сервлетах для таких переменных использовать именно это имя. Теперь можно перейти к краткому обзору интерфейса API, который используется при работе с объектом HttpSession.
Интерфейс HttpSession содержится в пакете javax.sevlet.http. В табл. 4.1 перечислены методы этого интерфейса согласно версии API 2.2 сервлетов.
Таблица 4.1. Методы класса HttpSession
Метод |
Возвращаемое значение |
Описание |
getAttribute (String name) |
Object |
Возвращает подключенный к сеансу объект с заданным именем name или null, если не найдено объекта с таким именем |
setAttribute (String name.Object obj) |
void |
Подключает к сеансу объект obj с именем name. Если к сеансу ранее был подключен другой объект с этим именем, прежняя связь теряется |
getAttribute Names() |
Enumeration |
Перечень объектов типа String, содержащий имена всех объектов, подключенных к сеансу |
removeAttri bute (String name) |
void |
Удаляет объект с указанным именем из сеанса |
getCreationTime() |
long |
Системное время (GMT) создания объекта такое же, как в System. currentTimeMillisO |
getLastAccessed Time() |
long |
Системное время последнего обращения клиента к сеансу. Формат такой же, как в getCreationTime |
getMaxInactive Interval() |
int |
Максимальный интервал времени (в секундах), в течение которого контейнер сервлета поддерживает сеанс открытым между обращениями к нему клиента |
setMaxInactive Interval (int interval) |
void |
Устанавливает интервал времени (в секундах) между обращениями клиента, по истечении которого контейнер сервлетов сделает данный сеанс недействительным |
invalidate() |
void |
Делает данный сеанс недействительным и прекращает все связи с объектами |
isNew() |
boolean |
Возвращает значение true, если клиент еще не знает о сеансе или клиент предпочел не присоединяться к сеансу. Обычно этот метод вызывается сразу после вызова метода getSession объекта HttpServlrtRequest |
getId() |
String |
Возвращает уникальный идентификатор, присвоенный данному сеансу |
В этой версии произошли некоторые изменения по сравнению с версией 2.1, которые необходимо указать, так как в некоторых процессорах сервлетов используется старая версия.
Методы getAttribute и setAttribute заменили прежние методы getValue и setValue. Метод getAttributeNames заменил прежний метод getVal ueNames. Эти изменения были проделаны в процессе общей модернизации спецификаций классов сервлетов.
Для хранения ссылки на объект сеанса в классе HttpSession и ее извлечения используется имя типа String, как в следующем примере, где session — это переменная класса HttpSession:
ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
if( cart == null ){ // предположительно первый проход
cart = new ShoppingCart();
session.setAttribute( "cart",cart );
}
Некоторые дополнительные изменения в API по сравнению с предыдущими версиями обусловлены соображениями безопасности. В версии API 2.1 можно было использовать метод getSessionContext для получения связанного объекта HttpSessionContext. Этот метод и интерфейс HttpSessionContext в нынешней версии отнесены к нерекомендуемым (deprecated), и они будут удалены из последующих версий этой библиотеки (иногда такие методы называются устаревшими).
В версии API 2.2 особое внимание уделено тому, чтобы существенная информация web-приложения оставалась в рамках этого приложения. В интерфейсе Servl etContext определены методы, которые сервлет может использовать для взаимодействия с контейнером и совместного использования одного и того же объекта с другими сервлетами этого приложения. Конкретный объект класса HttpSession может задействоваться более чем одним сервлетом, но только в случае, если эти сервлеты принадлежат тому же приложению. Участие сервлета или JSP-страни- цы в определенном приложении устанавливается с помощью параметров инициализации, которые использует процессор сервлетов.
Если механизм HttpSession только создавал бы атрибуты и не обеспечивал бы никакого способа избавления от них, процессор сервлетов вскоре исчерпал бы все ресурсы памяти. К счастью, имеется несколько методов организации приложения таким образом, чтобы избежать проблем с памятью.
Те процессоры сервлетов, с которыми мы работаем в наших примерах (JRun и Tomcat), по умолчанию удаляют объекты HttpSession, если они не использовались в течение 30 минут. Нужный интервал времени допустимого простоя можно установить для каждого приложения, задавая параметры инициализации в ядре сервлетов. В табл. 4.1 указан метод setMaxInactivelntarval, с помощью которого можно задать величину этого интервала. Установка отрицательного значения -1 означает, что для сеанса не задано время простоя, и в этом случае программист должен явным образом удалить объект с помощью метода invalidate.
Также можно явным образом удалить определенные объекты из класса HttpSession с помощью метода RemoveAttribute (в API 2.1 этот метод назывался remove Value). Программисту следует очень внимательно относиться к выбору объектов, которые он собирается хранить как объекты сеанса. Помните, что вы не можете предсказать, когда пользователь вернется к данному сеансу. Поэтому не рекомендуется хранение таких объектов, как, например, объекты соединения с базами данных, которые требуют значительных системных ресурсов.
Интерфейс HttpSessionBindingListener
Как вспомогательное средство для управления системными ресурсами, которые могут быть задействованы в сеансах, и как средство отладки в API сервлетов предусмотрены интерфейс HttpSessionBindingListener и класс HttpSessionBinding- Event. В этом интерфейсе определены два метода.
void valueBoundCHttpSessionBindingEvent event). Когда объект, реализующий интерфейс HttpSessionBindingListener, присоединяется к сеансу HttpSession, вызывается данный метод. Параметр этого метода event передает двоякую информацию — имя, которое было использовано для присоединения объекта к сеансу, и идентификатор сеанса (объект типа String).
void valueUnbound(HttpSessionBindingEvent event). Этот метод вызывается, когда объект должен быть удален из сеанса. Обычно это происходит, когда в результате простоя выполняется метод invalidate класса HttpSession и сеанс становится недействительным.
В сервлете catalogServ мы демонстрируем использование этого интерфейса для выполнения простых операций по отладке.
Корзина покупателя на языке Java
В этой главе мы создадим сервлет, функции которого ограничены отображением каталога и оформлением заказа на товары. В реальном коммерческом сайте эти функции составили бы лишь небольшую часть всех возможностей, предложенных пользователю. В нашем примере новый объект класса HttpSession создается, когда сервлет CatalogServ в первый раз получает запрос от данного пользователя. В реальном виртуальном магазине сеанс может быть создан в другой части сайта.
Возможной «точкой входа» посетителя может быть просмотр всего каталога или списка товаров какой-либо одной серии или просмотр результатов поиска по ключевому слову. В любом случае, мы не создаем объект ShoppingCart до тех пор, пока пользователь не проявит достаточного интереса к товару, чтобы просмотреть его полное описание. В этот момент мы создаем объект Cartltem для просматриваемого товара непосредственно из его описания в документе XML и добавляем объект Cartltem в ShoppingCart, то есть в корзину.
Пока пользователь просматривает информацию о различных товарах, мы создаем объекты Cartltem для каждого выбранного товара и удаляем из корзины те товары, которые он раздумал покупать. На любой странице пользователь имеет возможность вернуться к полному каталогу, найти товар по ключевому слову, просмотреть содержимое корзины или перейти к оплате товара. Теперь рассмотрим детали реализации описанной функциональности.
Классы Cartltem и ShoppingCart
Минимальным требованием к объекту, представляющему товар в корзине покупателя, является наличие идентификатора данного товара и количества заказанных экземпляров. Название товара, его цену и другие сведения можно найти в каталоге — документе XML. Но мы решили не ограничиваться минимальными требованиями и добавили в Cartltem название товара, цену и информацию по доставке, как показано в листинге 4.1.
Следует отметить несколько важных моментов, касающихся класса Cartltem. Во-первых, конструктор работает непосредственно с объектом El ement структуры DOM, представляющим данный товар. Это упрощает добавление различных дополнительных переменных в XML-каталог. Во-вторых, класс Cartltem реализует интерфейс Serializable. Это позволяет посылать коллекцию объектов Cartltem, представляющую собой список заказанных товаров, другой программе Java, используя сериализацию данных. Сериализация также требуется, если процессору сервлетов приходится хранить сеанс или пересылать его на другой сервер. Наконец, названия методов доступа, например getld и setNumberOrdered, соответствуют соглашению об именах, принятому в JavaBeans, чтобы упростить использование объекта Cartltem в коде JSP-страницы.
Листинг 4.1. Класс Cartltem (cartltem.java) [Все представленные в курсе тексты программ можно найти на сайте издательства по адресу www.piter.com. — Примеч. ред. ]
package com.XmlEcomBook.catalog; import java.util.* ; import java.io.* ; import org.xml.sax.* ; import org.w3c.dom.* ; public class CartItem implements java.io.Serializable { // be sure to change this if substantive variables change static final long serialVersionUID = 3260689382642549142L; // these are set from the constructor private String id ; // from product element private String name ; // from name element private String price ; // from price element private String shippingType ; // from shipping_info element private String shippingValue ; // may be null if type is special // these may change private int numberOrdered ; // changes public String getId(){ return id ;} public String getName(){ return name ; } public String getPrice() { return price ; } public String getShippingType() { return shippingType ; } public String getShippingValue() { return shippingValue ; } public int getNumberOrdered(){ return numberOrdered ; } public void setId(String s){ id = s ; } public void setName(String s){ name = s; } public void setPrice(String s){ price = s ;} public void setShippingType(String s ){shippingType = s ;} public void setShippingValue(String s) { shippingValue = s ;} public void setNumberOrdered( int n ){ numberOrdered = n ; System.out.println("setNumberOrdered " + n ); } // needed for operation as a Bean public CartItem(){ } // constructor uses a <product> org.w3c.dom.Element public CartItem( Element pe ){ id = pe.getAttribute("id"); NodeList nl = pe.getElementsByTagName( "name" ); name = nl.item(0).getFirstChild().getNodeValue() ; nl = pe.getElementsByTagName( "price" ); price = nl.item(0).getFirstChild().getNodeValue() ; nl = pe.getElementsByTagName( "shipping_info" ); Element ship = (Element) nl.item(0); shippingType = ship.getAttribute("type"); shippingValue = ship.getAttribute("value"); // may be "" } // handy for debugging public String toString() { StringBuffer sb = new StringBuffer("CartItem name:"); sb.append( name ); sb.append(" numberOrdered: "); sb.append( Integer.toString( numberOrdered )); return sb.toString(); } }
Класс ShoppingCart достаточно прост, так как все, что от него требуется, — манипулирование объектами класса Cartltem. Как показано в листинге 4.2, мы храним ссылки на объекты Cartltem в двух местах — Vector и HashTable. Причина этого заключается в том, что порядок размещения ссылок в объекте HashTable непредсказуем и может меняться по мере добавления новых товаров. Представляется разумным хранить эти ссылки в предсказуемой и воспроизводимой последовательности в объекте Vector и в то же время иметь возможность доступа к товарам по их идентификаторам через хэш-таблицу.
Заметим, что класс ShoppingCart реализует интерфейс Serializable, так что вся корзина (то есть объект класса Shoppi ngCart) может пересылаться между программами Java или записываться в файл посредством сериализации.
Листинг 4.2. Начало кода класса ShoppingCart (ShoppingCart.java)
package com.XmlEcomBook.catalog; import java.io.*; import java.util.* ; public class ShoppingCart implements java.io.Serializable { private Vector items ; // maintains order of selection of items private Hashtable itemsById ; public ShoppingCart(){ items = new Vector(); itemsById = new Hashtable(); } // items vector may be empty public Vector getItems(){ return items ; } // returns CartItem for this id or null if not in list public CartItem getProdById(String s ){ return (CartItem) itemsById.get( s ); } // CartItem is assumed to be unique public int addItem( CartItem x ){ items.addElement( x ); itemsById.put( x.getId() , x ); return items.size(); }
В листинге 4.3 показаны остальные методы класса ShoppingCart. Поскольку мы храним ссылки на объекты Cartltem в двух коллекциях, для удаления элемента из объекта HashTable применяется метод removeByld с указанием идентификатора товара и затем вызывается метод removeEl eraent вектора items.
Листинг 4.3. Остальная часть кода класса ShoppingCart (ShoppingCart.java)
// remove an item from the cart by product id public CartItem removeById( String s ){ CartItem ret = (CartItem)itemsById.get( s ); if( ret == null ) return null ; itemsById.remove(s); // remove by key items.removeElement( ret ); return ret ; } // remove all CartItem for which the numberOrdered is zero // returns the count of items left public int removeEmptyItems(){ Enumeration keys = itemsById.keys(); while( keys.hasMoreElements()){ String key = (String)keys.nextElement(); CartItem ci = (CartItem)itemsById.get(key); if( ci.getNumberOrdered() == 0 ){ removeById( key ); } } return items.size(); } // mainly for debugging public String toString() { StringBuffer sb = new StringBuffer( "ShoppingCart has " + items.size() + " items.\r\n" ) ; Enumeration e = items.elements(); while( e.hasMoreElements()){ sb.append("Item: "); sb.append( e.nextElement().toString() ); sb.append("\r\n"); } return sb.toString(); } }
Класс, который мы написали для отображения элементов каталога и манипулирования корзиной покупателя (классом ShoppingCart), называется CatalogServ. Здесь мы используем усовершенствованные версии классов для представления каталога сети, описанных в главе 3, и добавляем функции для организации корзины покупателя и мониторинга сеанса. Возможности сервлета CatalogServ перечислены ниже.
Для простоты будем считать, что заказ товара (добавление его в корзину и изменение количества заказанных экземпляров) может происходить только на странице с подробной информацией о данном товаре.
Кроме того, наш сервлет не будет выполнять никаких других функций, свойственных обычному коммерческому сайту. Как показано на рис. 4.1, наш сервлет просто размещает текст «Your site navigation could go here» [Здесь может быть расположен интерфейс для навигации по вашему сайту. — Примеч. перев. ], в то время как в этом месте обычного коммерческого сайта, как правило, находятся логотипы и навигационный интерфейс.
Рис. 4.1. Отображение полного каталога с помощью класса CatalogServ
Перед тем как углубляться в изучение исходного кода CatalogServ, рассмотрим некоторые другие представления, которые он генерирует. На рис. 4.2 показана страница с раскрывающимся списком ключевых слов. Это тот же самый список, который представлен на рис. 3.1 в главе 3, но сервлет CatalogServ добавляет в нижнюю часть страницы дополнительные ссылки:
Full Catalog (Весь каталог);
Books (Книги);
CDs (Компакт-диски):
Widgets (Приборы и устройства);
Search (Поиск).
Рис. 4.2. Отображение раскрывающегося списка ключевых слов
В табл. 4 2 представлен перечень команд отображения (значений параметра action), которые определяют ответ сервлета CatalogServ Команды, управляющие отображением, обычно модифицируются дополнительными параметрами
Таблица 4.2. Команды, распознаваемые сервлетом CatalogServ
Команда (параметр action) |
Дополнительные параметры |
Отображение |
showcatalog |
Параметр select = "all" |
Таблица со списком всех товаров (см. рис. 4.1) |
showcatalog |
Параметр select =одна из серий товаров (books, CDs, widgets) |
Таблица со списком товаров определенной серии (см. рис. 4.3) |
selectkeyword |
Параметр select = "all" |
Раскрывающийся список ключевых слов (см. рис. 4.2) |
showproduct |
Параметр id из формы |
Полная информация о товаре (см. рис. 4 4) |
keywdsearch |
Параметр keyword из формы |
Список товаров с этим ключевым словом |
setcart |
Параметры id и itemct из формы |
Полная информация о товаре с измененным количеством заказанных экземпляров (см. рис. 4.5) |
showcart |
Параметры отсутствуют |
Список всех товаров в корзине покупателя с указанием количества заказанных экземпляров (см. рис. 4.6) |
Как видно из листинга 4.4, в классе CatalogServ имеется некоторое количество статических переменных, которые определяют различные ресурсы Мы приводим типичные значения этих переменных В реальном сервлете эти значения заменяются специфичными для системы значениями, хранящимися в файле catalog.properties, откуда они считываются методом imt Мы будем использовать класс Properties — расширение класса Hashtable из пакета java.util, где содержатся очень удобные методы для загрузки текстовых параметров из файла
Листинг 4.4. Статические переменные и метод mit в сервлете CatalogServ (CatalogServ.java)
package com.XmlEcomBook.catalog; import java.io.*; import java.util.* ; import javax.servlet.*; import javax.servlet.http.*; public class CatalogServ extends HttpServlet { static String brcrlf = "<br />\r\n" ; static String version = "1.03 Oct 17, 2000"; static String cssLinkA = "<link rel=\"stylesheet\" href=\"" ; // following is part of a web server URL for the style sheet static String cssLinkB = "XmlEcommBook/catalog/catalog.css" ; static String cssLinkC = "\" type=\"text/css\" media=\"screen\" >" ; static String resourcepath = "XmlEcommBook/catalog/" ; static String host = "http://localhost/"; // these are servlet engine aliases static String servlet = "servlet/catalog" ; static String checkout = "servlet/checkout" ; // these are complete webserver paths static String cssLink = cssLinkA + host + cssLinkB + cssLinkC ; static String alias ; // for catalog servlet static String checkoutalias ; static String resources ; // for images, style sheets, etc String catPath = "e:\\scripts\\XMLgifts" ; // for xml String catName = "catalog.xml" ; Properties catProp = new Properties(); public void init(ServletConfig config) throws ServletException { try { super.init(config); System.out.println("CatalogTestServ init called, version " + version ); String tmp = config.getInitParameter("workdir"); if( tmp != null ) catPath = tmp ; File f = new File( catPath, "catalog.properties"); if( f.exists() && f.canRead() ){ FileInputStream fis = new FileInputStream(f) ; catProp.load( fis ); fis.close(); tmp = catProp.getProperty("csspath"); if( tmp != null ) cssLinkB = tmp; tmp = catProp.getProperty("host"); if( tmp != null ) host = tmp ; tmp = catProp.getProperty("resourcepath" ) ; if( tmp != null ) resourcepath = tmp ; tmp = catProp.getProperty("catalogservlet"); if( tmp != null ) servlet = tmp ; tmp = catProp.getProperty("checkoutservlet" ); if( tmp != null ) checkout = tmp ; } else { System.out.println ("CatalogServ can't read catalog.properties"); } resources = host + resourcepath ; alias = host + servlet ; checkoutalias = host + checkout ; System.out.println( "resources:" + resources ); System.out.println("servlet: " + alias ); System.out.println("checkout: " + checkoutalias ); CatalogBean.setTheCatalog( catPath, catName ); CatalogBean.setResourcePath( resources ); }catch( Exception e ){ System.out.println("CatalogTestServ init " + e ); } }
Заметим, что метод init вызывает два статических метода из класса CatalogBean. Вызов метода setTheCatal од необходим для считывания данных из файла XML, а метод setTheResoursePath устанавливает путь, который будет использоваться для нахождения таких ресурсов, как изображения товаров. Определения класса Gala- togBean вы найдете далее в этой главе в разделе «Класс CalatogBean».
Все запросы, выполняющиеся методом GET, просто перенаправляются методу doPost, как показано в листинге 4.5. Метод doPost объединяет стандартный раздел HEAD файла HTML и результаты выполнения трех методов — doPageTop, doPage- Mid и doPageEnd. Заметим, что обращение к методу getSession с параметром true приводит к созданию нового сеанса, то есть объекта HttpSession, если он еще не существует для данного пользователя.
Для того чтобы продемонстрировать методику отладки, мы проверим, является ли данный сеанс новым. Если сеанс окажется новым, мы присоединим объект CartListener и напечатаем идентификатор нового сеанса. Функции объекта CartLis- tener мы обсудим позже. Все инструкции, связанные с отладкой, будут удалены из финальной версии приложения, так как конструирование и вывод на печать объекта Date требуют больших затрат времени.
Листинг 4.5. Методы doGet и doPost (CatalogServ.java)
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost( req, resp ); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); outputHead( out ); HttpSession session = req.getSession(true); if( session.isNew() ){ session.putValue( "listener", new CartListener() ); // session.setAttribute( "listener", new CartListener() ); System.out.print("Session is new " + session.getId() + " " + new Date().toString() ); } try { doPageTop( req, resp, out, session ); doPageMid( req, resp, out, session ); doPageEnd( req, resp, out, session ); }catch( Exception e ){ e.printStackTrace( out ); } out.println("</body>"); out.println("</html>"); out.close(); } private void outputHead( PrintWriter out ){ out.println("<html>"); out.println("<head><title>Catalog Information</title>"); out.println( cssLink ); out.println("</head>\r\n<body>"); } // compose and output all material at the top of the page public void doPageTop( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session ){ out.print("<h1>XMLgifts</h1>"); out.print("<h2>Your Site Navigation Could Go Here</h2>\r\n"); }
В этом примере метод doPageTop очень прост, но в реальном коммерческом сайте этот метод можно использовать для отображения средств навигации по сайту и выполнения других функций.
В этом примере метод doPageEnd (листинг 4.6) просто создает набор активных ссылок в нижней части страницы. Ссылки на различные варианты представления каталога присутствуют всегда, но некоторые ссылки, связанные с корзиной покупателя, появляются, только если в корзину добавлены какие-либо товары. Обратите внимание, мы используем метод removeEmptyElements, чтобы гарантировать, что переменная nitem правильно отражает содержимое корзины.
Листинг 4.6. Метод doPageEnd (CatalogServ.java)
public void doPageEnd( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session ){ ShoppingCart cart = (ShoppingCart)session.getValue("cart"); String a1 = "<a href=\"" + alias + "?action=" ; int nitem = 0 ; // permit checkout if cart has any items out.print("<center>"); if( cart != null && (nitem = cart.removeEmptyItems()) > 0 ){ // out.print( cart.toString()); // debugging out.print( brcrlf ); out.print( a1 +"showcart\" > Show Cart (" + nitem + " items)</a> " ); out.print("<a href=\"" + checkoutalias + "?action=initial\" >Checkout Now</a> \r\n"); } out.println( a1 + "showcatalog&select=all\" > Full Catalog</a> "); String[] prodL = CatalogBean.getCat().getProductLineNames(); for( int i = 0 ; i < prodL.length ; i++ ){ out.print( a1 + "showcatalog&select=" + prodL[i] + "\" >"); out.println( " " + prodL[i] + " </a> "); } out.print( a1 + "selectkeyword&select=all\" > Search </a>" ); out.print("</center>\r\n"); out.println("<hr><center>" + version + "</center>\r\n"); }
Метод doPageMid управляет ответом сервлета на запрос пользователя. Значение параметра action определяет выбор метода представления, который, в свою очередь, генерирует требуемое представление. Как показано в листинге 4.7, последовательность инструкций if определяет, какой из методов вызывается.
Листинг 4.7. Метод doPageMid (CatalogServ.java)
public void doPageMid( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session ){ String action = req.getParameter("action"); String select = req.getParameter("select"); if( "showcatalog".equals( action )){ if( select == null || select.equals("all") ){ completeCatalog( out ); } else { productLineCatalog( out, select ); } } else if( "selectkeyword".equals( action )){ if( select == null || select.equals("all") ){ doKeywordSelect( out ); } } else if( "keywdsearch".equals( action )) { String keyword = req.getParameter("keyword"); if( keyword != null ){ keywordCatalog( out, keyword ); } } else if( "showproduct".equals( action ) || "setcart".equals( action) ){ doShowProduct( req, resp, out, session, action ); }else if( "showcart".equals( action ) ){ doShowCart( req, resp, out, session, action ); } }
В методах completeCatalog (листинг 4.8) и productLineCatalog (листинг 4.9) используется один и тот же подход к генерированию таблицы, содержащей соответствующий список товаров (либо полный, либо одну серию). Основное различие заключается в том, что в методе completeCatalog, как видно на рис. 4.1, в таблице имеются три столбца — по одному на каждую серию товаров. Для выбора одной из этих серий следует вызвать метод setlnitialSelect класса CatalogBean. В случае если у вас имеется более четырех серий товаров, следует искать какой- нибудь другой метод отображения каталога, так как таблица с пятью и более столбцами будет выглядеть не слишком хорошо.
Текст HTML, относящийся к конкретному товару, будет иметь вид, подобный следующему:
<а class="ch3"
href="http://localhost/serviet/catalog?асtion=
showproduct&id=bk0022">
Guide to Plants </a> <span class="ch4">price ea = $12.99 </span>
Этот код делает название товара активной ссылкой, которая отсылает параметры action и id сервлету, что ведет к отображению полной информации о данном товаре. Атрибут class контролирует тип, размер и цвет шрифта различных фрагментов текста путем выбора той или иной таблицы стилей из файла catalog.css.
Листинг 4.8. Методы doKeywordSelect и completeCatalog (CatalogServ.java)
public void doPageMid( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session ){ String action = req.getParameter("action"); String select = req.getParameter("select"); if( "showcatalog".equals( action )){ if( select == null || select.equals("all") ){ completeCatalog( out ); } else { productLineCatalog( out, select ); } } else if( "selectkeyword".equals( action )){ if( select == null || select.equals("all") ){ doKeywordSelect( out ); } } else if( "keywdsearch".equals( action )) { String keyword = req.getParameter("keyword"); if( keyword != null ){ keywordCatalog( out, keyword ); } } else if( "showproduct".equals( action ) || "setcart".equals( action) ){ doShowProduct( req, resp, out, session, action ); }else if( "showcart".equals( action ) ){ doShowCart( req, resp, out, session, action ); } }
Метод productLineCatalog, как показано в листинге 4.9, формирует таблицу, содержащую только один столбец. После того как внешний вид таблицы HTML определен, переменная line используется для вызова метода setlnitialSelect, который выбирает одну из серий товаров. Затем мы просто совершаем итерации по выбранному списку товаров и используем метод doListOutput для отображения форматированного текста HTML по каждому товару. Затем мы закрываем таблицу. Типичный результат представлен на рис. 4.3.
Листинг 4.9. Метод productLineCatalog (CatalogServ.java)
public void productLineCatalog( PrintWriter out, String line ){ CatalogBean cb = new CatalogBean(); out.println("<h2>" + line + " Catalog</h2>"); out.println("<table width=\"90%\" border=\"3\" align=\"center\" >"); out.println("<thead><tr><th>" + line + "</th>" + "</tr></thead>"); out.println("<tbody><tr valign=\"top\"><td>"); String link = alias + "?action=showproduct" ; cb.setInitialSelect( line ); int ct = cb.getSelectedCount(); out.println("We have " + ct + " items." + brcrlf ); cb.setOutput("short", link); for( int i = 0 ; i < ct ; i++ ){ out.println( cb.doListOutput(i) ); out.println( brcrlf );out.println( brcrlf ); } out.println("</td></tr></table>"); }
Рис. 4.3. Отображение одной серии товаров
В методе keywordCatal og (листинг 4.10) применяется тот же принцип, но выбор происходит на основе переменной keyword, которая передается в качестве параметра методу setKeywordSel ect класса CalatogBean.
Листинг 4.10. Метод keywordCatalog отображает только элементы, содержащие выбранное ключевое слово (CatalogServ.java)
public void keywordCatalog( PrintWriter out, String keyword ){ CatalogBean cb = new CatalogBean(); out.println("<h2>Selected by " + keyword + " Catalog</h2>"); out.println("<table width=\"90%\" border=\"3\" align=\"center\" >"); out.println("<thead><tr><th>" + keyword + " </th>" + "</tr></thead>"); out.println("<tbody><tr valign=\"top\"><td>"); String link = alias + "?action=showproduct" ; cb.setKeywordSelect( keyword ); int ct = cb.getSelectedCount(); out.println("We have " + ct + " items." + brcrlf ); cb.setOutput("short", link); for( int i = 0 ; i < ct ; i++ ){ out.println( cb.doListOutput(i) ); out.println( brcrlf );out.println( brcrlf ); } out.println("</td></tr></table>"); }
Отображение полной информации о товаре
Метод doShowProduct класса Catal ogServ отвечает за отображение полной информации об одном конкретном товаре. Существует несколько вариантов формата этого отображения в зависимости от наличия в X ML-каталoгe ссылки на изображение этого товара и от того, находится ли уже данный товар в корзине покупателя.
На рис. 4.4 приведено типичное представление, сгенерированное для товара, которого еще нет в корзине покупателя и для которого имеется изображение. Кнопка, расположенная в правом нижнем углу, предназначена для добавления в корзину этого товара. При щелчке на этой кнопке генерируется запрос к сервле- ту, параметр action принимает значение setcart, а параметр itemct — значение 1, в результате в корзине оказывается один экземпляр данного товара.
Рис. 4.4. Отображение полной информации о товаре вместе с его изображением
Рис. 4.5. Информация о товаре без его изображения
Если товар уже был заказан покупателем и находится в корзине, то страница с информацией о нем включает поле текущего количества заказанных экземпляров товара и позволяет его изменять. Пример такой страницы показан на рис. 4.5, причем для данного товара не предусмотрено изображения. Если пользователь щеллкает на кнопке Change (Изменить), в запросе к сервлету значение параметра action будет равно setcart, а значение itemct — введенному числу.
Теперь рассмотрим подробно, как устроен метод doShowProduct, код которого приведен в листинге 4.11. Этот метод отображает подробную информацию о данном товаре и позволяет добавлять товар в корзину. В первую очередь этот метод должен получить экземпляр класса Choppi ngCart. В приведенном коде используется метод getAttribute класса HttpSession, который является предпочтительным по отношению в устаревшему методу getValue. Закомментированная строка показывает, как использовать метод getValue, если в вашем случае процессор сервлетов ориентирован на версию 2.1 API.
Когда вызывается метод doShowProduct, в объекте HttpServletRequest всегда будет содержаться параметр id, который однозначно идентифицирует отображаемый товар.
Когда пользователь щелкает на кнопке (такой, как на рис. 4.4 или 4.5), вызывается метод doShowProduct со значением параметра action, равным setcart. В этом случае значение параметра itemct интерпретируется как новое количество заказанных экземпляров данного товара.
Когда вызывается метод doShowProduct с параметром action, равным showproduct, количество заказанных товаров (numberOrdered) не изменяется.
Листинг 4.11. Метод doShowProduct, который отображает информацию о товаре и позволяет заказывать товар (CatalogServ.java)
public void doShowProduct( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session, String action ){ ShoppingCart cart = (ShoppingCart)session.getValue("cart"); // older servlet engines use getValue // ShoppingCart cart = session.getAttribute("cart"); // API 2.2 if( cart == null ){ // presumably the first pass cart = new ShoppingCart(); session.putValue("cart", cart ); // older //session.setAttribute( "cart",cart ); // API 2.2 } out.print( brcrlf ); CatalogBean cb = new CatalogBean(); String id = req.getParameter( "id" ); if( "setcart".equals( action ) ){ String tmp = req.getParameter("itemct"); int itemct = 0; try { itemct = Integer.parseInt( tmp ); }catch(NumberFormatException e){ System.out.println("doShowProduct " + e ); } CartItem item = cart.getProdById( id ); if( item == null ){ item = cb.createCartItem( id ); cart.addItem( item ); } item.setNumberOrdered( itemct ); cart.removeEmptyItems(); } out.print("<table width=\"70%\" border=\"3\" align=\"center\" >\r\n"); cb.doFullItem( id, out, cart, alias ); out.print("</table>\r\n"); }
Если action = showcart, то вызывается метод doShowCart. Как показано в листинге 4.12, этот метод переписывает теги HTML, которые задают начало и конец таблицы. Строки таблицы заполняются с помощью метода doCartList класса CatalogBean.
Листинг 4.12. Метод doShowCart (catalogServ.java)
public void doShowCart( HttpServletRequest req, HttpServletResponse resp, PrintWriter out, HttpSession session, String action ){ ShoppingCart cart = (ShoppingCart)session.getValue("cart"); // older servlet engines use getValue // ShoppingCart cart = session.getAttribute("cart"); // API 2.2 if( cart == null ){ out.println("Serious problem with session data" + brcrlf ); return ; } CatalogBean cb = new CatalogBean(); String link = alias + "?action=showproduct" ; cb.setOutput("short", link); out.print("<table width=\"90%\" border=\"3\" align=\"center\" >"); cb.doCartList( out, cart ); out.print("</table>\r\n"); }
Использование класса carttistener
Поскольку процессор сервлетов во многом определяет поведение объекта HttpSession, решение проблем отладки, связанных с сеансами, может оказаться непростым делом. API сервлетов предоставляет интерфейс HttpSessionBindingListener и класс HttpSessionBindingEvent для решения проблем отладки и для управления ресурсами, которые могут быть присоединены к объекту HttpSession.
В листинге 4.13 показан простой пример использования этого интерфейса в классе CartLi stener, который является внутренним по отношению к классу Catalog- Serv. В этом примере мы просто записываем системное время присоединения объекта к объекту HttpSession, и затем, когда сеанс закрывается, печатаем время жизни объекта. Объект CartLi stener присоединяется к объекту ShoppigCart, когда он создается в первый раз методом doPost (см. листинг 4.5).
Листинг 4.13. Внутренний класс CartListener (CatalogServ.java)
class CartListener implements HttpSessionBindingListener { long created ; public void valueBound( HttpSessionBindingEvent evt ){ created = System.currentTimeMillis(); } public void valueUnbound( HttpSessionBindingEvent evt ){ long del = System.currentTimeMillis() - created ; System.out.println( "Session lifetime: " + ( del / 1000 ) + " seconds "); } } }
Как вы уже, вероятно, заметили при обсуждении методов класса CatalogServ, вся основная работа по созданию форматированного отображения информации о товарах выполняется методами класса CatalogBean. Эти методы форматирования выделены из сервлета в отдельный класс для того, чтобы упростить реализацию функций каталога в технологии JavaServer Pages. В этой главе мы за недостатком места не приводим JSP-версию каталога, а технологии JSP посвящена глава 5.
Большинство методов класса CatalogBean, используемых в этой главе, остались такими же, как в главе 3. Наиболее значительные изменения касаются объектов ShoppingCart и Cartltem. Напомним, что в CatalogBean имеется статическая переменная для объекта TheCatalog, который управляет объектной моделью документа, созданной на основе каталога (файла XML). В обновленной версии CatalogBean мы сделали эту переменную закрытой (private) и предложили следующий метод доступа:
private static TheCatalog cat ;
static void setTheCatalog( String path, String name ){
File f = new File( path, name );
cat = new TheCatalog( f, null, null );
cat.scanCatalog();
}
static TheCatalog getCat(){ return cat ; }
Мы также добавили статическую переменную resoursePath и метод setResourse- Path, который задает значение этой переменной при инициализации сервлета:
private static String resourcePath ; // используется для
// изображений, звуковых
// файлов и т.д.
static void setResourcePath( String s ){ resourcePath = s ; }
Использование объектов Cartltem и ShoppingCart
Как показано в листинге 4.14, CatalogBean создает объект Cartltem для некоторого товара на основе информации, содержащейся в экземпляре cat класса TheCatalog.
Метод doCartLnst, приведенный в листинге 4 14, контролирует вывод всех объектов Cartltem, содержащихся в экземпляре класса ShoppingCart Форматирование таблицы осуществляется методом doShowCart класса CatalogServ Метод doCartList обеспечивает отображение каждого заказанного товара в отдельной строке таблицы HTML, а также отображение количества заказанных экземпляров (numeberOrdered) для каждого из них Типичная страница, показывающая содержимое корзины покупателя, представлена на рис 4 6
Листинг 4.14. Методы CatalogBean, связанные с объектом ShoppingCart (CatalogBean.java)
public CartItem createCartItem( String id ){ Element pE = cat.getProductElByID( id ); return new CartItem( pE ); } // we are in a <table>.. </table> pf was created with setOutput public void doCartList( PrintWriter out, ShoppingCart cart ){ Vector v = cart.getItems(); int ct = v.size(); for( int i = 0 ; i < ct ; i++ ){ CartItem item = (CartItem)v.elementAt(i); out.print("<tr><td>"); String id = item.getId(); out.print( pf.doListOutput( cat.getProductElByID(id))); out.print( "</td><td>"); out.print( "Number ordered: " + item.getNumberOrdered() ); out.print( "</td></tr>\r\n"); } }
Рис. 4.6. Отображение содержимого корзины покупателя
Сложное форматирование при представлении полной информации о товаре (как показано на рис. 4 4 и 4 5) контролируется методом doFuTIItem, код которого приведен в листинге 4 15 Исходно мы строим таблицу с четырьмя или тремя ячейками в зависимости от наличия или отсутствия изображения товара В этой таблице содержатся сведения о товаре, его изображение (если оно есть), название и цена, а также количество заказанных экземпляров (если покупатель решил приобрести этот товар) Создается объект ProductFormatter, задающий стиль отображения всей этой информации о товаре, то есть стиль всех фрагментов текста, которые содержатся в документе XML, описывающем данный товар
Обычно объект Cartltem, соответствующий идентификатору товара, в этот момент уже добавлен к корзине Но если это не так, то создается новый объект Cartltem
Наличие или отсутствие ссылки на изображение товара в XML-каталоге влияет на форматирование верхней строки таблицы Если изображение отсутствует, то название товара вместе с его описанием занимает всю верхнюю строку
Листинг 4.15. Метод doFullItem (CatalogBean.java)
// we are in a <table>.. </table> public void doFullItem( String id, PrintWriter out, ShoppingCart cart, String alias ){ pf = new ProductFormatter( "full" ); pf.setResourcePath( resourcePath ); Element pE = cat.getProductElByID( id ); // out.print( cart.toString() ); // debugging if( pE == null ){ out.print("Bad Product ID " + id ); return ; } CartItem ci = cart.getProdById( id ); if( ci == null ){ ci = new CartItem( pE ); System.out.println("Create CartItem " + ci.toString() ); } String imgS = pf.doImageTag( pE ); if( imgS != null ){ out.print("<tr><td align=\"center\">"); out.print(pf.doImageTag( pE )); out.print("</td><td align =\"left\" >"); } else{ // no image, spread description out.print("<tr><td align=\"center\" colspan=\"2\" >"); } out.print(pf.doProdName( pE )); out.print(pf.doAuthorArtist( pE )); out.print(pf.doDescription( pE )); out.print("</td></tr>\r\n"); out.print("<tr><td align=\"center\" >"); out.print(pf.doPrice( pE )); // lower right cell contains form out.print("</td><td align=\"center\" > <form method=\"POST\" action=\""); out.print( alias ); out.print( "\" >\r\n" ); out.print("<input type=\"HIDDEN\" name=\"id\" value=\""); out.print( id ); out.print("\" >"); out.print("<input type=\"HIDDEN\" name=\"action\"" + " value=\"setcart\" >"); if( ci.getNumberOrdered() == 0 ){ out.print("<input type=\"HIDDEN\" name=\"itemct\" value=\"1\" >"); out.print("<input type=\"SUBMIT\" value=\"" ); out.print("Add this item to cart\" >"); } else { out.print( "<i>To change the number ordered, enter a new number here " + "and click the Change button.</i><br />" ); out.print("<input type=\"TEXT\" name=\"itemct\" size=\"5\"" + " value=\"" + ci.getNumberOrdered() + "\" > "); out.print("<input type=\"SUBMIT\" value=\""); out.print("Change\" >"); } out.print("</form></td></tr>\r\n"); }
Мы добавили множество методов в класс ProductFormatter, описанный в главе 3. Вообще говоря, эти методы просто расширяют те возможности, которыми уже обладал этот класс. В листинге 4.16 показаны статические переменные, которые определяют два различных стиля. «Краткий» (short) стиль используется для отображения списков товаров (всего каталога или одной серии), а полный (full) стиль используется в методе doFullItem класса CatalogBean для отображения полной информации об одном товаре. Также мы создали коллекцию fieldHash, которая связывает названия товаров с целочисленными константами.
Листинг 4.16. Статические переменные класса ProductFormatter (ProductFormatter.java)
package com.XmlEcomBook.catalog; import java.util.* ; import java.io.* ; import org.xml.sax.* ; import org.w3c.dom.* ; public class ProductFormatter { static String brcrlf = "<br />\r\n" ; // xhtml style br static String[] shortEl = { "prname", "price" // for product name }; static String[] shortSt = { "ch3", "ch4" }; // as used in doListOutput static String[] fullEl = { "prname", "author","artist","description", "price" // for product name }; static String[] fullSt = { "ch3", "au1", "au1", "ch4", "ch4" }; static Hashtable fieldHash ; // field names for lookup static String[] fields = { "id", "keywords", "prname", "price", "author", "artist", "description", "image", "caption", "quantity_in_stock", "onsale_date", "shipping_info" } ; static { fieldHash = new Hashtable() ; for( int i = 0 ; i < fields.length ; i++ ){ fieldHash.put( fields[i], new Integer( i ) ); } }
В листинге 4.17 показано начало кода для методов и переменных экземпляра и конструктора ProductFormatter. Заметим, что конструктор задает формат в соответствии с переданной ему переменной frmt, которая может принимать значение "short" или "full".
Листинг 4.17. Начало кода методов и переменных экземпляра (Product Formatter.java)
String[] elem, style ; String resourcePath ; String aLink ; int linkN ; // when aLink is supplied, it should be something like // "/servlet/catalog?action=showproduct", then the doListOutput will build a // complete link adding &id=xxxxxx to attach to the first parameter public void setALink(String s, int pos ) { aLink = s ; linkN = pos ;} public void setResourcePath( String s ){ resourcePath = s ; } // throws exception if unknown format public ProductFormatter( String frmt ){ if( frmt.equals("short")){ elem = shortEl ; style = shortSt ; } else if( frmt.equals("full")){ elem = fullEl ; style = fullSt ; } else { throw new IllegalArgumentException ("ProductFormatter: " + frmt ); } }
Методы, вызываемые методом doFullItem
Теперь рассмотрим методы, которые используются для полного описания товара. Эти методы, приведенные в листинге 4.18, вызываются методом doFullItem класса CatalogBean.
Листинг 4.18. Методы, используемые для полного описания товара (Product Formatter.java)
public String doImageTag( Element el ){ NodeList nl = el.getElementsByTagName( "image" ); int ct = nl.getLength(); if( ct == 0 ) { return null ; } Element img = (Element)nl.item(0); StringBuffer sb = new StringBuffer( ); addText( sb, "image", img ); return sb.toString(); } // element is the complete product public String doProdName( Element el ){ NodeList nl = el.getElementsByTagName( "name" ); if( nl.getLength() == 0 ) return ""; StringBuffer sb = new StringBuffer( ); sb.append( "<span class=\""); sb.append( "ch3" ); sb.append("\">"); addText( sb, "prname", (Element)nl.item(0) ); sb.append( " </span><br />"); return sb.toString(); } // element is the complete product public String doAuthorArtist( Element el ){ NodeList unl = el.getElementsByTagName( "author" ); NodeList rnl = el.getElementsByTagName( "artist" ); if( rnl.getLength() == 0 && unl.getLength() == 0 ) return ""; StringBuffer sb = new StringBuffer( ); int i ; int ct = rnl.getLength(); if( ct > 0 ){ sb.append("<span class=\"au1\" >" ); if( ct == 1 ) sb.append( "<i>Artist:</i> " ); else sb.append("<i>Artists:>/i> "); for( i = 0 ; i < ct ; i++ ){ addText( sb, "artist",(Element) rnl.item(i) ); if( ct > 1 && ( i + 1) < ct ) sb.append(", "); } sb.append("<br />"); } ct = unl.getLength(); if( ct > 0 ){ sb.append("<span class=\"au1\" >" ); if( ct == 1 ) sb.append( "<i>Author:</i> " ); else sb.append("<i>Authors:</i> "); for( i = 0 ; i < unl.getLength() ; i++ ){ addText( sb, "author",(Element) unl.item(i) ); if( ct > 1 && ( i + 1) < ct ) sb.append(", "); } sb.append("<br />"); } return sb.toString(); } // element is the complete product public String doDescription( Element el ){ NodeList nl = el.getElementsByTagName( "description" ); if( nl.getLength() == 0 ) return "No Description Available"; StringBuffer sb = new StringBuffer( ); addText( sb, "description",(Element) nl.item(0) ); return sb.toString(); } public String doPrice( Element el ){ NodeList nl = el.getElementsByTagName( "price" ); if( nl.getLength() == 0 ) return "Contact XMLgifts"; StringBuffer sb = new StringBuffer( ); addText( sb, "price", el ); return sb.toString(); }
Метод doListOutput (листинг 4.19) вызывается из CatalogBean для создания объекта типа String, содержащего форматированные данные по отдельному товару. Этот метод используется при конструировании таблицы, подобной приведенной на рис. 4.3. Обратите внимание, что мы создаем объект StringBuffer для построения строки (объекта String), поскольку добавление новых фрагментов в StringBuffer гораздо эффективнее, чем конкатенация (объединение) объектов String. Также заметим, что если методу передается переменная aLink, то текст, соответствующий элементу с индексом linkN, отображается в виде гипертекстовой ссылки.
Листинг 4.19. Метод doListOutput (ProductFormatter.java)
// создается строка с данными об отдельном товаре,
// используемая во многих листингах
// внешний вид определяется содержимым elem style
// обычно добавляется ссылка на более подробную
// информацию
public String doListOutput( Element el ){ StringBuffer sb = new StringBuffer( ); String pid = null ; if( aLink != null ){ pid = "&id=" + el.getAttribute("id") ; } for( int i = 0 ; i < elem.length ; i++ ){ if( i == linkN && pid != null ){ sb.append( "<a class=\"" ); sb.append( style[i] ); sb.append("\" href=\""); sb.append( aLink ); // typically "http://xxxhost/servlet/serv sb.append( pid ); sb.append("\">"); addText( sb, elem[i], el ); sb.append( " </a>"); } else { sb.append( "<span class=\""); sb.append( style[i] ); sb.append("\">"); addText( sb, elem[i], el ); sb.append( " </span>"); } } return sb.toString(); } // end doListOutput
Метод addText, приведенный в листинге 4.20, вызывается некоторыми другими методами из класса ProductFormatter. Ему передается объект StringBuffer, в который добавляется текст. Параметр name определяет, какой именно текст требуется добавить, а элемент Element соответствует некоторому товару в каталоге. Целое число, являющееся значением параметра name, управляет точками перехода в инструкции switch.
Листинг 4.20. Метод addText (ProductFormatter.java)
// заметим, что в большинстве случаев нам нужно
// значение узла
private void addText(StringBuffer sb, String name, Element el ){ Object obj = fieldHash.get( name ); if( obj == null ){ sb.append( "no " + name + " found " ); return ; } switch( ((Integer)obj).intValue()){ case 0 : // "id", addID( sb, el ); break ; case 1 : // "keywords", case 2 : // "prname", product name addProductName( sb, el ); break ; case 3 : // "price" addPrice( sb, el ); break ; case 4 : // "author", addAuthor( sb, el ); break ; case 5 : // "artist", addArtist( sb, el ); break ; case 6 : // "description", addExtendedText( sb, el ); break ; case 7 : // "image", addImageTag( sb, el ); break ; case 8 : // "caption" addExtendedText( sb, el ); break ; case 9 : // "quantity_in_stock", case 10 : // "onsale_date" } }
Различные методы, вызываемые методом addText, приведены в листингах 4.21 и 4.22. Эти методы извлекают тот или иной текст из элемента product и добавляют его в Stri ngBuffer.
Листинг 4.21. Различные методы, вызываемые методом addText (ProductFormatter.java)
// Элемент е - это товар <product>
private void addID(StringBuffer sb, Element e ){ String id = e.getAttribute("id" ); sb.append("product code: "); if( id.length()== 0 ){ sb.append("not assigned"); } else { sb.append( id ); } } // element is either a <product> or <name> as child of a product private void addProductName( StringBuffer sb, Element e){ if( !e.getNodeName().equals("name") ){ NodeList nl = e.getElementsByTagName( "name" ); e = (Element) nl.item(0); } sb.append( getChildrenText( e ) ); } // element is <author> tag private void addAuthor( StringBuffer sb, Element e){ NodeList nl = e.getElementsByTagName( "name" ); sb.append( getChildrenText( (Element) nl.item(0)) ); } private void addArtist( StringBuffer sb, Element e){ NodeList nl = e.getElementsByTagName( "name" ); sb.append( getChildrenText((Element) nl.item(0)) ); } // example <author><name>Christoph Minwich</name></author> // known to have price private void addPrice( StringBuffer sb, Element e ){ NodeList nl = e.getElementsByTagName( "price" ); sb.append("price ea = "); sb.append( nl.item(0).getFirstChild().getNodeValue() ); }
Служебный метод getChildrenText, приведенный в листинге 4.22, собирает вместе текст всех дочерних узлов данного элемента.
Листинг 4.22. Служебный метод getChildrenText (ProductFormatter.java)
private String getChildrenText( Element e ){ StringBuffer sb = new StringBuffer(); NodeList nl = e.getChildNodes(); for( int i = 0 ; i < nl.getLength() ; i++ ){ sb.append( nl.item(i).getNodeValue() ); } return sb.toString(); }
Метод addlmageTag, приведенный в листинге 4.23, использует информацию из тега XML <iraage>. Ниже приводится пример из файла catalog.xml:
<image format="gif" width="234" height="4Q0"
src="images/covers/pi ants.gif">
<caption>
<paragraph>This is the cover from the
first edition.</paragraph>
</caption>
</image>
Помимо создания тега <img>, который вставляет в HTML-страницу изображение, этот метод также проводит анализ и отображает подпись к изображению.
Листинг 4.23. Метод, создающий теги изображения (ProductFormatter.java)
private void addImageTag( StringBuffer sb, Element img ){ String format = img.getAttribute("format"); String width = img.getAttribute("width"); String height = img.getAttribute("height"); String src = img.getAttribute("src"); String desc = "image "; sb.append("<img src=\""); // detect option for image source to point off site if( !src.toUpperCase().startsWith("HTTP")){ sb.append(resourcePath ); } if( sb.charAt( sb.length() - 1 ) == '/' && src.charAt(0) == '/' ){ sb.append( src.substring(1) ); } else sb.append( src ) ; sb.append( "\" alt=\"" ); NodeList imgNL = img.getElementsByTagName("caption"); if( imgNL.getLength() > 0 ){ sb.append( desc ); // addText(sb, "caption", (Element) imgNL.item(0) ); } else sb.append( desc ); sb.append( "\" width=\"" ); sb.append( width ); sb.append( "\" height=\"" ); sb.append( height ); sb.append( "\" >" ); //NodeList imgNL = img.getElementsByTagName("caption") ; if( imgNL.getLength() == 0 ) return ; Element caption = (Element) imgNL.item(0); addText( sb, "caption", caption ); return ; }
Формат нашего XML-каталога допускает использование стиля running_text в подписях и описаниях товаров. Методы addExtendedText и doExtendedTextEl ement, показанные в листинге 4.24, могут объединить весь текст описания или подписи с соответствующей разметкой HTML, чтобы создать абзац или назначить начертание этого текста (курсив или полужирный шрифт).
Листинг 4.24 Метод addExtendedText()
// одним из вариантов стиля является
// <paragraph>, простой текст
private void addExtendedText( StringBuffer sb, Element e ){ NodeList nl = e.getChildNodes(); int ct = nl.getLength(); // sb.append("child count " + ct + brcrlf ); for( int i = 0 ; i < ct ; i++ ){ Node n = nl.item(i); switch( n.getNodeType() ){ case Node.TEXT_NODE : sb.append( n.getNodeValue().trim() ); break ; case Node.ELEMENT_NODE : Element en = (Element) n ; // sb.append("Element Name " + en.getNodeName() ); doExtendedTextElement(en.getNodeName(), sb, en ); break ; default : sb.append("default Name " + n.getNodeName() ); sb.append(" Value " + n.getNodeValue() ); } sb.append(' ' ); // because values get trimmed //sb.append( brcrlf ); } } // private void doExtendedTextElement( String name, StringBuffer sb, Element e){ if( name.equals("paragraph") ){ sb.append("<p>"); addExtendedText( sb, e ); sb.append("</p>"); } else if( name.equals("italics")){ sb.append("<i>" ); addExtendedText( sb, e ); sb.append(" </i>"); } else if( name.equals("bold")){ sb.append("<b>" ); addExtendedText( sb, e ); sb.append(" </b>"); } else { addExtendedText( sb, e ); } } public String toString() { StringBuffer sb = new StringBuffer("ProductFormatter "); return sb.toString(); } }