В лекции 2 мы создали определение типа документа (DTD) каталога товаров для нашего гипотетического сетевого магазина XMLGifts.com. В лекции 3 мы создали JSP-страницы и сервлеты для просмотра этого каталога. Теперь нам необходимо обеспечить возможность обновления информации в каталоге, в том числе удаления из него данных о некоторых товарах и добавления данных о новых товарах. Операции такого типа объединены общим названием CRUD (Create, Read, Update, Delete — создание, чтение, обновление, удаление).
Так как данные в нашем каталоге представлены в виде кода 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-страницы, которые будут отображать информацию для пользователя и обрабатывать введенную им информацию. При этом информация будет извлекаться из объектов данных и с помощью браузера отображаться в формах. После заполнения форм пользователем информация отправится обратно на сервер. На этом этапе также потребуется программа, которая получит эту информацию и обновит объекты данных.
Иногда взаимодействие с документом 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. Эти методы очень полезны при отладке, но не являются составной частью классов, поэтому не включены в описание программ, чтобы в этой лекции вы сосредоточились на основной части кода.
Класс 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>" ); } }
Класс 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; } }
Следующий класс, 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>" ); }
}
Элемент 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>" ); }
}
Очередной класс, который мы рассмотрим, — это класс 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>" ); } }
Элемент 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 используется для представления даты. Он основан на следующем фрагменте 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>" ); } }
В классе 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 ); }
}
Код для представления информации пользователю
Теперь нам нужен код для представления пользователю всей информации, содержащейся в классах данных. Необходимо также обеспечить пользователю возможность создавать, редактировать и удалять элементы каталога. Это делается с помощью кода HTML, JSP-страниц и сервлетов. JSP-страницы и сервлеты генерируют код HTML, который отображается в браузере пользователя. Для работы с каталогом пользователь выбирает HTML-страницу, которая обеспечивает возможности создания, редактирования и удаления элементов каталога. На этой странице есть ссылки на сервлет, вызывающий JSP-страницу, которая представляет редактируемую форму HTML, предназначенную для того, чтобы пользователь вносил в нее новую информацию о товарах или изменял старую.
Всю оставшуюся часть этой лекции мы посвятим изучению кода HTML, JSP- страниц и сервлетов, которые обеспечивают эту функциональность.
На главной 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>
Класс 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>"); } }
Страница 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>
Страница 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; } %>
Сервлет 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-каталога товаров небольшого сайта.