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

         

Добавление свежих новостей


Важной особенностью этого приложения является возможность добавления новых сообщений без нарушения нормальной работы web-сайта Эту функцию иллюстрирует верхний правый угол рис. 8 1. Вместо того чтобы модифицировать DOM в памяти сервера, сервлет CompanyNewsServ записывает модифицированную версию исходного файла XML на диск. Этот обновленный файл сообщений будет автоматически загружен в очередной раз при вызове DOM из библиотеки DOMlibrary.



Гибкость отображения


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

заголовки новостей со ссылкой на полный текст;

несколько наиболее интригующих строк со ссылкой на полный текст;

полный текст сообщения.

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



Информация для управления сообщениями


Поскольку вы или ваши служащие будут обновлять страницу новостей в режиме подключения к сети, было бы полезно отслеживать, кто какое сообщение написал. (Например, чтобы знать, кто должен получать нагоняй за допущенную ошибку.) Для этого используется атрибут <author> элемента <Newsitem>. Сервлет обновления сообщений, описанный в разделе «Добавление свежих новостей» этой главы, обладает простым механизмом контроля доступа, в котором используются имя автора и пароль; этот то самое имя автора, которое становится значением атрибута author.

Чтобы создать документ HTML, в котором заголовок содержит ссылку на полную версию текста, нужно использовать уникальный идентификатор. Мы, например, выбрали простейший вариант: при создании каждого сообщения <NewsItem> ему присваивается серийный номер, который и становится значением его атрибута id.

 





Использование класса NewsFormatter


В этом разделе рассматриваются два способа использования класса NewsFormatter: с сервлетом общего назначения TheNewsServ и с JSP-страницами.

 



Элементы текстов сообщений


Хотя в области искусственного интеллекта и понимания компьютером естественных языков сделаны большие успехи, никто не рассчитывает, что компьютер напишет хороший заголовок, проанализировав текст сообщения. Поэтому приходится согласиться с тем, что нужен человек, который для каждого способа представления новостей создаст отдельный заголовок. В сообщении обычно указывается дата, а иногда рядом еще и место, где произошло то событие, о котором идет речь, например: «Остин, Техас, 1 января 2000». Эта задача тоже должна выполняться человеком.

Некоторые сообщения очень выигрывают, когда сопровождаются графикой, звуковыми клипами или ссылками на другие сайты, поэтому продумайте, как сконструировать документ XML, чтобы его можно было дополнить различными элементами, способствующими увеличению привлекательности страницы новостей. Мы решили, что было бы слишком сложно и неудобно снабжать систему показа новостей в нашем примере всеми возможными «украшениями». Поэтому мы будем хранить тексты сообщений для второго и третьего способов (краткое и полное сообщение), используя тег XML <[[CDATA...]]>.

Поскольку анализаторы XML не пытаются анализировать текст, содержащийся внутри раздела СВАТА, в этот раздел вы можете поместить любую разметку HTML, не сбивая с толку анализатор.

Элементы, содержащие дату, заголовок, краткое сообщение и полный текст, показаны в листинге 8.1.

Листинг 8.1. Дата, заголовок, короткое и полное сообщения (thenews.xml)

<date>Austin, TX, Jun 14 2000</date>

<head>Best Seller at a Great Price</head>

<short>

<![CDATA[Due to a special deal with the publisher, we can now offer

<i>Dryer Lint Art</i>

at 50% off the retail price.]]>

</short>

<long>

<![CDATA[This books starts with simple Dryer Lint projects

suitable for the novice and advances through easy

stages tothe (literally)

<b>monumental</b>

recreation of famous monuments in that most flexible of craft


materials, dryer lint. Even though you may never attempt major

constructions like the Statue of Liberty project documented in

the final chapter, your projects will benefit by a study of this

famous creation. Includes DHL diagrams.]]>

</long>

Другим аспектом гибкости является способность выборочно представлять сообщения в соответствии с темой, интересующей посетителя. Предполагая, что спектр возможных интересов посетителей сайта XMLGifts.com очень широк, мы хотим показать каждому посетителю те новости, которые связаны с его излюбленной темой, и в том месте сайта, куда он с наибольшей вероятностью заглянет. В такой структуре неизбежны перекрывающиеся области; например, книга о музыкальной группе может оказаться интересной как для покупателей книг, так и для покупателей музыкальных компакт-дисков. Следовательно, каждое сообщение должно быть снабжено одной или несколькими пометками, которые указывают, к каким тематическим категориям можно его отнести; а формат представления новостей должен допускать переключения между различными темами сообщений.

Для того чтобы пометить сообщение и отнести его тем самым к определенной категории, мы можем использовать элемент или атрибут. Следуя советам, приведенным в разделе «Элементы или атрибуты?» главы 2, можно заключить, что в данном случае лучше использовать атрибуты, так как тема сообщения — это данные о содержимом элемента, и мы предполагаем, что количество тем сообщений будет ограниченным.

 




Класс NewsFormatter


Ключевым классом Java для формирования новостных сообщений является класс NewsFormatter. Как показано в листинге 8.3 и следующих листингах, класс NewsFormatter включает в себя объект Fi I e, который указывает на исходный файл XML. Класс NewsFormatter использует класс DOMlibrary, описанный в главе 7, чтобы получить объект Document, содержащий все данные из файла XML. Конструктор получает список узлов NodeList, содержащий все узлы Newsitem, и задействует его для создания массива с названием itemNodes. Этот массив требуется для решения различных задач форматирования.

Листинг 8.3. Начало кода класса NewsFormatter

package com.XmlEcomBook.Chap08;

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

public class NewsFormatter { static String handler ; // the servlet for single item presentation public static void setHandler(String s){handler=s; }

// instance variables File newsFile ; String newsFileName ; String newsFilePath ; String headStr, footStr ;

Node[] itemNodes ; Element docRoot ; Hashtable nodeHash ; // <Newsitem Elements keyed by tag name

int maxNitems, skipNitems; int itemsCount = 0 ;

public NewsFormatter( File f ) throws IOException { newsFile = f ; newsFileName = f.getAbsolutePath() ; int p = newsFileName.lastIndexOf( File.separatorChar ); if( p > 0 ){ newsFilePath = newsFileName.substring(0,p); } else { System.out.println("NewsFormatter path problem"); } DOMlibrary library = DOMlibrary.getLibrary(); Document doc = library.getDOM( newsFileName ); if( doc == null ){ throw new FileNotFoundException( newsFileName ); } docRoot = doc.getDocumentElement(); NodeList newsItemNodes = doc.getElementsByTagName("Newsitem"); int ct = newsItemNodes.getLength(); itemNodes = new Node[ ct ]; for( int i = 0 ; i < ct ; i++ ){ itemNodes[i] = newsItemNodes.item( i ); } }

Вы, должно быть, помните из главы 7, что класс DOMIibrary перезагружал файл XML, если время его последней модификации изменялось. Поскольку в нашем случае объект Document не меняется в результате действия класса NewsFormatter, он может использоваться совместно любым количеством сервлетов и доступ к нему нужно синхронизировать.


У нас имеются две версии метода doNews. Версия, приведенная в листинге 8.4, используется для вывода нескольких сообщений в виде заголовков новостей, краткого и полного форматов изложения. Эта версия метода обеспечивает следующие возможности: выбор сообщений по их тематике и времени появления, пропуск указанного количества сообщений и ограничение общего количества отображаемых сообщений. Строки hs и fs — необязательные параметры, которые обеспечивают некоторые небольшие дополнительные возможности форматирования.

Метод doNews проверяет наличие параметров типа Srting, которые ограничивают выбор сообщений определенными тематическими или временными рамками. Если параметр topstr отличен от null и не пуст, вызывается метод selectNodes, который ограничивает полный список сообщений набором новостей, соответствующим заданной тематике. Аналогично, если указана строка age, вызывается метод limitAge. Если какой-либо из этих методов сокращает список сообщений до нуля, метод doNews сразу же прекращает свое выполнение. Другие параметры контролируют максимальное количество новостей на странице и относительный номер сообщения, с которого начинается их просмотр.

Листинг 8.4. Метод doNews выбирает способ представления сообщений (NewsFormatter.java)

// hs and fs are head and foot used in short and long display // you can also specify templates in the <Newsfile element // PrintWriter, hs, fs, topics, H,S or L, age, mx# // skpN is used to skip the first N items that qualify // presumably printed elsewhere on the page, use 0 to see all // returns number of news items printed public int doNews( PrintWriter out, String hs, String fs, String topstr, String sz, String age, int skpN, int mxN ){ headStr = hs ; footStr = fs ; skipNitems = skpN ; maxNitems = mxN ; itemsCount = 0 ; if( topstr != null && topstr.length() > 0 ){ if( selectNodes(topstr, out )== 0 ) return 0 ; } if( age != null && age.length() > 0 ){ if( limitAge( age, out ) == 0 ) return 0 ; } char szch ; if( sz == null || sz.length() == 0 ) szch = 'L' ; // default to long form else szch = sz.toUpperCase().charAt(0); switch( szch ) { case 'H' : doHeadlineNews( out ); break ; case 'S' : doShortNews( out ); break ; case 'L' : default : doLongNews(out ); } return itemsCount ; }



Метод doNews, показанный в листинге 8.5, отыскивает сообщение по указанному атрибуту id и форматирует полную версию сообщения. Оставшийся метод класса NewsFormatter предназначен для поддержки двух методов doNews.



Листинг 8.5. Версия doNews для одного выбранного сообщения (NewsFormatter.java)

// version to do a single item by id - always full length public int doNews( PrintWriter out, String hs, String fs, String id ){ headStr = hs ; footStr = fs ; itemsCount = 0 ; Node n = null ; // for( int i = 0 ; i < itemNodes.length ; i++ ){ n = itemNodes[i]; // <Newsitem nodes String nid = ((Element)n).getAttribute("id"); if( id.equals( nid )){ break ; } } // if not located by id, will be oldest item findNodes((Element) n ); // locates the parts of <Newsitem doNewsItemLong( out ); // with the single id return itemsCount ; }

Мы решили, что заголовки сообщений всегда будут форматироваться как маркированные списки (unordered lists) HTML. Это очень упрощает метод doHeadli - neNews, показанный в листинге 8.6.



Листинг 8.6. Метод, форматирующий список заголовков новостей (NewsFormatter.java)

// Headline always formatted as <UL> with link public void doHeadlineNews(PrintWriter out){ out.println( "<ul>" ); for( int i = skipNitems ; i < itemNodes.length ; i++ ){ if( i >= maxNitems ) break ; Node n = itemNodes[i]; // <Newsitem nodes String id = ((Element)n).getAttribute("id"); findNodes((Element) n ); // locates the parts of <Newsitem out.print("<li><a href=" + handler + "?id=" + id + "&size=L >" ); out.print( nodeHash.get("head") ); out.println("</a></li>"); } out.println("</ul>"); }

Метод doShort, показанный в листинге 8.7, проверяет наличие заданного по умолчанию шаблона форматирования короткой версии сообщения, а затем выводит эту версию на страницу. Обратите внимание на то, что из каждого элемента (сообщения) извлекается его атрибут id, прежде чем будет вызван метод doNewsItemShort. Этот идентификатор впоследствии присоединяется к каждому элементу, представляющему собой краткую версию, в качестве ссылки на полный текст сообщения.





Листинг 8.7. Метод doShortNews (NewsFormatter.java)

public void doShortNews(PrintWriter out){ NamedNodeMap attrib = docRoot.getAttributes(); Node n = attrib.getNamedItem( "shorttemplate") ; String template = null ; if( n != null ) template = n.getNodeValue(); if( headStr == null && template != null && template.length() > 2 ){ try { setFromTemplate( template ); System.out.println("Template set ok " + headStr + footStr ); }catch(IOException ie ){ System.out.println("Unable to read " + template ); } } out.println( headStr ); for( int i = skipNitems ; i < itemNodes.length ; i++ ){ if( i >= maxNitems ) break ; n = itemNodes[i]; // <Newsitem nodes String id = ((Element)n).getAttribute("id"); findNodes((Element) n ); // locates the parts of <Newsitem doNewsItemShort( out, id ); } out.println( footStr ); }

Как показано в листинге 8.8, метод doLongNews проверяет наличие заданного по умолчанию шаблона форматирования полной версии сообщения, после чего выполняет цикл по всем сообщениям в массиве itemNodes.



Листинг 8.8. Метод doLongNews выводит полный текст сообщения (NewsFormatter.java)

public void doLongNews(PrintWriter out){ NamedNodeMap attrib = docRoot.getAttributes(); Node n = attrib.getNamedItem( "longtemplate"); String template = null ; if( n != null ) template = n.getNodeValue(); if( headStr == null && template != null && template.length() > 2 ){ try { setFromTemplate( template ); System.out.println("Template set ok " + headStr + footStr ); }catch(IOException ie ){ System.out.println("Unable to read " + template ); } } out.println( headStr ); for( int i = skipNitems ; i < itemNodes.length ; i++ ){ if( i >= maxNitems ) break ; n = itemNodes[i]; findNodes((Element) n ); doNewsItemLong( out ); } out.println( footStr ); }

В листинге 8.9 показан метод limitAge, который вызывается всегда, когда в методе doNews присутствует строка, задающая максимально допустимый «возраст» новостей. После проверки корректности целочисленного значения, содержащегося в строке age, этот метод заново компонует массив itemNodes, помещая туда только выбранные сообщения.





Листинг 8.9. Метод, выбирающий сообщения по дате их создания (NewsFormatter.java)

// limit to only most recent entries - return number, may be zero private int limitAge(String age, PrintWriter out ){ int days = 100 ; try { days = Integer.parseInt( age ); if( days <= 0 ) days = 1 ; }catch(NumberFormatException nfe){ return itemNodes.length ; // no change } int today =(int)( System.currentTimeMillis() /( 24 * 60 * 60 * 1000)); int oldest = today - days ; Vector v = new Vector( itemNodes.length ); int nidate = today ; // in case of parse problem int i ; for( i = 0 ; i < itemNodes.length ; i++ ){ Node n = itemNodes[i]; // <Newsitem nodes String t = ((Element)n).getAttribute("timestamp"); try { nidate = Integer.parseInt( t ); }catch(Exception nfe){ // number format or null pointer System.out.println( "NewsFormatter.limitAge " + nfe ); } if( nidate >= oldest ){ v.addElement( n ); } } itemNodes = new Node[ v.size() ]; // may be zero for( i = 0 ; i < v.size(); i++ ){ itemNodes[i] = (Node) v.elementAt(i); } return itemNodes.length ; }

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

Листинг 8.10. Метод, который выбирает сообщения по указанным темам (NewsFormatter.java)

// based on String with topics separated by commas // example attribute topics="general,books,java" // output capability only used for debugging private int selectNodes(String topics, PrintWriter out ){ Hashtable recognize = new Hashtable(); StringTokenizer st = new StringTokenizer ( topics.toUpperCase(), ","); while( st.hasMoreTokens()){ String tmp = st.nextToken().trim(); recognize.put( tmp,tmp ); } // hashtable can now be used to recognize selected topics Vector v = new Vector( itemNodes.length ); int i ; for( i = 0 ; i < itemNodes.length ; i++ ){ Node n = itemNodes[i]; // <Newsitem nodes String t = ((Element)n).getAttribute("topic"); st = new StringTokenizer(t.toUpperCase(),","); while( st.hasMoreElements()){ // we just use hashtable get to see if topic is present if( recognize.get( st.nextToken().trim() ) != null ){ v.addElement(n); break; } } // end while over topic list } // end loop over all nodes // build new array from selected nodes itemNodes = new Node[ v.size() ]; for( i = 0 ; i < v.size(); i++ ){ itemNodes[i] = (Node) v.elementAt(i); } return itemNodes.length ; }



Метод findNodes, показанный в листинге 8.11, вызывается для каждого сообщения, которое должно быть помещено на страницу. Входной элемент Element — это узел Newsltem документа XML. Метод findNodes создает переменную nodeHash, которая позволяет другим методам извлекать дочерние элементы Newsltem, например <short>, из коллекции nodeHash. Ключами элементов в этой хэш-таблице являются имена узлов.



Листинг 8.11. Метод findNodes класса NewsFormatter (NewsFormatter.java)

// locate the nodes that are Elements for text data private void findNodes( Element ne ){ NodeList nl = ne.getChildNodes(); // all nodes int ct = nl.getLength(); nodeHash = new Hashtable( 2 * ct ); for( int i = 0 ; i < ct ; i++ ){ Node n = nl.item(i); if( n instanceof Element ){ nodeHash.put( n.getNodeName(), n ); } } }

Заголовки и краткая версия сообщения всегда снабжаются ссылкой на полную версию. Эта ссылка встраивается в HTML-страницу с помощью методов doNewsItemHead и doNewsItemShort, как показано в листинге 8.12.



Листинг 8.12. Методы doNewsItemHead и doNewsItemShort (NewsFormatter.java)

// <Newsitem has been hashed, id is attribute private void doNewsItemHead( PrintWriter out, String id ){ out.print("<a href=" + handler + "?id=" + id + "&size=L >" ); out.print("<h3>"); out.print( nodeHash.get("head") ); out.println("</h3></a>"); out.println(); }

// <Newsitem has been hashed, id is attribute // output with <p>..</p> formatting private void doNewsItemShort( PrintWriter out, String id ){ // note anchor to full item display out.print("<a href=" + handler + "?id=" + id + "&size=L >" ); out.print("<h3>"); out.print( nodeHash.get("head") ); out.println("</h3></a>"); Element de = (Element)nodeHash.get("date"); out.print( de.getFirstChild() ); out.println("</p>"); Element ne = (Element)nodeHash.get("short"); String wrk = ne.getFirstChild().getNodeValue().trim() ; if( !(wrk.startsWith("<P") || wrk.startsWith("<p")) ){ out.print("<p>"); } out.print( wrk ); if( !(wrk.endsWith("/p>") || wrk.endsWith("/P>"))){ out.print("</p>"); } itemsCount++ ; out.println(); }



Как показано в листинге 8.13, метод doNewsItemLong форматирует текст заголовка с помощью тега <h3>. Было бы неплохо усовершенствовать этот метод так, чтобы он допускал возможность изменять указанный формат по мере надобности. Основной текст сообщения форматируется как абзац с помощью тега <р>. Внутри самого текста могут содержаться любые форматирующие теги HTML, но теги <р> всегда будут использоваться для полного текста сообщения.



Листинг 8.13. Метод doNewsItemLong выводит полную версию сообщения (NewsFormatter.java)

// <Newsitem elements have been hashed // output long form with <p>...</p> formatting private void doNewsItemLong( PrintWriter out ){ out.print("<h3>"); out.print( nodeHash.get("head") ); out.println("</h3>"); Element de = (Element)nodeHash.get("date"); out.print( de.getFirstChild() ); out.println("</p>"); Element ne = (Element)nodeHash.get("long"); String wrk = ne.getFirstChild().getNodeValue().trim() ; if( !(wrk.startsWith("<P") || wrk.startsWith("<p")) ){ out.print("<p>"); } out.print( wrk ); if( !(wrk.endsWith("/p>") || wrk.endsWith("/P>"))){ out.print("</p>"); } itemsCount++ ; out.println(); }

Наконец, в листинге 8.14 представлены два служебных метода. Метод setFor- matTempl ate отыскивает файл и считывает его строка за строкой. Предполагается, что в файле имеется строка, начинающаяся с текста "<!-INSERT". Она разделяет разметку HTML на два раздела, которые становятся переменными headStr и footStr. Метод toString предназначен для помощи в отладке.



Листинг 8.14. Конец исходного кода класса NewsFormatter (NewsFormatter.java)

private void setFromTemplate(String template ) throws IOException { File f = new File( newsFilePath, template ); FileReader fr = new FileReader( f ); BufferedReader br = new BufferedReader( fr ); StringBuffer hsb = new StringBuffer( 100 ); StringBuffer fsb = new StringBuffer( 100 ); String tmp = br.readLine(); // strips line terminators while( !tmp.startsWith("<!--INSERT" )){ hsb.append( tmp ); fsb.append("\r\n"); tmp = br.readLine(); } tmp = br.readLine(); while( tmp != null ){ fsb.append( tmp ); fsb.append("\r\n"); tmp = br.readLine(); } headStr = hsb.toString(); footStr = fsb.toString(); }

public String toString() { StringBuffer sb = new StringBuffer("NewsFormatter item ct= "); sb.append( Integer.toString( itemNodes.length )); return sb.toString() ; }

}






Класс NewsUpKeep


Классу NewsUpKeep передаются существующий объект DOM <Newsfile> и различные текстовые строки, которые составляют новый элемент <Newsitem>; он переписывает файл XML с сообщениями. Применить этот метод проще, чем создать элемент <Newsitem> и вставить его модель DOM, которая постоянно находится в памяти; кроме того, указанный метод гарантирует, что файл новостей будет обновлен правильным образом. Также при использовании этого метода исключается возможность того, что один из отображающих методов получит частично измененную модель DOM.

В листинге 8.27 показано начало кода класса NewsUpKeep. Конструктор класса использует полное имя файла для получения DOM из DOMIibrary. Поскольку эта модель DOM не модифицируется при переписывании файла XML, вам не нужно беспокоиться по поводу возможности одновременного доступа к этому объекту класса NewsFormatter.

Заметим, что в классе NewsUpKeep создается переменная rootNNM типа NamedNodeMap, которая содержит имена и значения атрибутов корневого элемента документа XML, <Newsfile>. Также конструктор помещает все узлы <Newsitem> в массив itemNodes.

Листинг 8.27. Начало класса NewsUpKeep (NewsUpKeep.java)

package com.XmlEcomBook.Chap08;

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

public class NewsUpkeep { File newsFile ; String newsFileName ; Node[] itemNodes ; NamedNodeMap rootNNM ; // for root attributes

public NewsUpkeep( File f) throws IOException { newsFile = f ; newsFileName = f.getAbsolutePath() ; DOMlibrary library = DOMlibrary.getLibrary(); Document doc = library.getDOM( newsFileName ); if( doc == null ){ throw new FileNotFoundException( newsFileName ); } Element re = doc.getDocumentElement(); rootNNM = re.getAttributes(); System.out.println("Root has " + rootNNM.getLength() + " attributes"); NodeList newsItemNodes = doc.getElementsByTagName("Newsitem"); int ct = newsItemNodes.getLength(); itemNodes = new Node[ ct ]; for( int i = 0 ; i < ct ; i++ ){ itemNodes[i] = newsItemNodes.item( i ); } }


В листинге 8. 28 показаны некоторые вспомогательные методы, необходимые в классе NewsUpKeep. Метод formatTopics гарантирует, что строка, которая будет записана в качестве значения атрибута topic, имеет правильный формат.



Листинг 8.28. Различные вспомогательные функции класса NewsUpKeep (NewsUpKeep.java)

//ensure there are no leading or trailing spaces on the // individual topics, comma separated, general,food , etc private String formatTopics(String s ){ if( s.indexOf(',') < 0 ) return s.trim(); // only separator is comma StringTokenizer st = new StringTokenizer( s, "," ); StringBuffer sb = new StringBuffer( s.length() ); while( st.hasMoreTokens() ){ sb.append( st.nextToken().trim() ); if( st.hasMoreTokens() ) sb.append(','); } return sb.toString(); } // convert system millisecs to days since epoch private String timeInDays(){ long t = System.currentTimeMillis() ; int tid = (int)(t / ( 1000 * 60 * 60 * 24 )); return Integer.toString( tid ); }

// s expected to be decimal number used in <Newsitem id= private String incrementID(String s ){ try{ int n = Integer.parseInt( s ); return Integer.toString( n + 2 ); }catch(NumberFormatException e){ return s + "a" ; } } public String toString() { StringBuffer sb = new StringBuffer("NewsUpkeep "); sb.append(" Newsitem count: " ); sb.append( Integer.toString( itemNodes.length )); return sb.toString(); }

Теперь мы подходим к основному рабочему методу, addltem. Сначала этот метод создает новый файл с временным именем и записывает туда стандартное объявление XML и комментарии. Затем создается тег <Newsfile>, куда записываются имена атрибутов и их значения из коллекции rootNNM.

Как видно из листинга 8.29, атрибут nextid обрабатывается специальным образом. Сохраняется текущее значение, которое становится значением атрибута id нового элемента <Newsitem>, а увеличенное значение записывается в тег <Newsfile>.

Листинг 8.29. Начало метода addltem (NewsUpKeep.java)

// items are always added at the top of the file // so we have to rebuild the start of the root element public void addItem( String head, String date, String topics, String author, String shrtStr, String longStr ) throws IOException { String idVal = "" ; String tmpfile = newsFileName + "$$" ; File f = new File( tmpfile ); FileWriter fw = new FileWriter(f); PrintWriter out = new PrintWriter( new BufferedWriter( fw ) ); out.println("<?xml version=\"1.0\" standalone=\"yes\" ?>"); out.println("<!-- output by NewsUpkeep -->"); int ct = rootNNM.getLength(); if( ct == 0 ){ out.println("<Newsfile>"); } else { out.print("<Newsfile "); for( int i = 0 ; i < ct ; i++ ){ Node an = rootNNM.item(i); String name = an.getNodeName(); String val = an.getNodeValue(); out.print( name + "=\"" ); if( name.equals("nextid") ){ idVal = val ; val = incrementID( val ); } out.print( val + "\" "); } out.println(" >"); }



Затем, как показано в листинге 8.30, пишется новый тег <NewsItem>, за которым следует заголовок сообщения, указывается дата и приводятся краткая и полная версии текста сообщения. Для того чтобы записать старые элементы <NewsItan>, вызывается метод writeNewsNode. После закрытия временного файла старый файл XML удаляется, а временный файл получает имя. Следующий раз, когда этот файл будет запрошен, класс DOMIibrary по изменившейся метке даты модификации файла (timestamp) определит, что нужно считывать новый файл.



Листинг 8.30. Метод addltem, продолжение (NewsllpKeep.java)

out.print("<Newsitem timestamp=\""); out.print( timeInDays() + "\" topic=\""); out.print( formatTopics( topics ) ); out.println( "\" author=\"" + author + "\" id=\"" + idVal + "\" >"); // end of <Newsitem .. > out.println("<head>" + head.trim() + "</head>" ); out.println("<date>" + date.trim() + "</date>" ); out.println("<short><![CDATA["); out.println( shrtStr.trim() ); out.println("]]></short>"); out.println("<long><![CDATA["); out.println( longStr ); out.println("]]></long>"); out.println("</Newsitem>"); for( int i = 0 ; i < itemNodes.length ; i++ ){ writeNewsNode(out, (Element)itemNodes[i] ); } out.println("</Newsfile>"); out.flush(); out.close(); File forig = new File( newsFileName ); DOMlibrary library = DOMlibrary.getLibrary(); // to prevent overlapping XML file operations synchronized( library ){ forig.delete(); if( !f.renameTo( forig )){ System.out.println("NewsUpkeep.addItem rename failed") ; } } }

Метод writeNewsNode, который записывает отдельный элемент <Newsitem>, показан в листинге 8.31.



Листинг 8.31. Метод, который записывает отдельный элемент из DOM (NewsllpKeep.java)

// write a <Newsitem Element duplicating the attributes public void writeNewsNode(PrintWriter out, Element e) { NamedNodeMap nnm = e.getAttributes(); out.print("<Newsitem " ) ; //timestamp=\""); int i ; for( i = 0 ; i < nnm.getLength() ; i++ ){ Attr na = (Attr) nnm.item(i); // Attr extends Node String atr = na.getName(); String val = na.getValue(); out.print( atr ); out.print("=\""); out.print( val ); out.print("\" "); } out.println(">"); NodeList nl = e.getChildNodes(); int ct = nl.getLength(); for( i = 0 ; i < ct ; i++ ){ Node nde = nl.item( i ); if( nde instanceof Element ){ Element ce = (Element)nde; String name = ce.getTagName(); out.print("<" + name + ">"); NodeList chnl = ce.getChildNodes() ; if( chnl.getLength() == 0 ) continue ; Node chn = chnl.item(0); if( name.equals("long") || name.equals("short") ){ out.print("<![CDATA["); out.println( chn.getNodeValue().trim() ); out.print("]]>"); } else { out.print( chn.getNodeValue() ); } out.println("</" + name + ">"); } } // loop over <Newsitem> child nodes out.println("</Newsitem>"); } }

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


Код для сервлета TheNewsServ


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

Листинг 8.15. Начало исходного кода сервлета TheNewsServ (TheNewsServ)

package com.XmlEcomBook.Chap08 ;

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

public class TheNewsServ extends HttpServlet { static String workDir = "E:\\scripts\\CompanyNews" ; static String newsFile = "thenews.xml" ; static String handler = "http://localhost/servlet/thenews" ; static String propfile = "conewserv.properties"; static String version = "v1.0"; static String pversion = "" ; static Properties cnProp ; static String brcrlf = "<br />\r\n" ; static String defaultHead = "<html>\r\n" + "<head><title>Company News Servlet</title></head>\r\n" + "<body>\r\n" + "<h2>Here is the news</h2>\r\n" ; static String defaultFoot = "</body></html>\r\n";

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

Листинг 8.16. Метод init класса TheNewsServ (TheNewsServ.java)

public void init(ServletConfig config) throws ServletException { super.init(config); String tmp = config.getInitParameter("workdir"); if( tmp != null ) workDir = tmp ; tmp = config.getInitParameter("propfile"); if( tmp != null ) propfile = tmp; System.out.println("Start TheNewsServ using " + workDir ); File f = new File( workDir, propfile ); try { cnProp = new Properties(); cnProp.load( new FileInputStream(f) ); tmp = cnProp.getProperty("thenewshandler"); if( tmp != null ) handler = tmp ; pversion = cnProp.getProperty("version"); if( pversion != null ){ defaultFoot = "<hr><br>News Servlet " + version + " properties: " + pversion + "<br>\r\n" + "</body>\r\n</html>\r\n" ; } NewsFormatter.setHandler( handler ); System.out.println( new Date().toString() + " Loaded properties for TheNewsServ: " + handler ); }catch(IOException e){ System.out.println("Error loading " + e ); }


}

Функциональность сервлета сконцентрирована в методе doGet, как видно из листинга 8.17. В запросе можно передать значения параметров, определяющих тему сообщений, максимальный «возраст» сообщений, требуемый способ представления и идентификатор сообщения. Заметим, что создается объект Fi I e, соответствующий файлу XML с сообщениями, и передается конструктору NewsFormatter. Использование объекта File гарантирует, что соблюдаются соглашения относительно разделителей для компонентов пути; NewsFormatter не открывает этот файл, но использует его имя при получении объектной модели документа для этого файла из DOMlibrary.



Листинг 8.17. Метод doGet (TheNewsServ.java)

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String topics = req.getParameter("topic"); String ageStr = req.getParameter("days"); String len = req.getParameter("size" ); // "S","H" or "L" String id = req.getParameter("id"); // a single item is requested try { File f = new File( workDir, newsFile ); NewsFormatter nf = new NewsFormatter( f ); if( id != null ){ nf.doNews( out, defaultHead,defaultFoot, id ); } else { // PrintWriter, head, foot, topics, H,S or L, age, skip#, mx# nf.doNews( out, defaultHead, defaultFoot, topics, len, ageStr,0, 10 ); } out.close(); }catch(Exception e){ System.err.println("TheNewsServ.doGet " + e ); errorMsg( out, "TheNewsServ.doGet", e ); } }

Обратите внимание, что конструкция try-catch в методе doGet направляет все исключения методу errorMsg, показанному в листинге 8.18. Разумеется, вам следует вставить свой адрес электронной почты в текст сообщения либо текст этого сообщения может состоять из специальной строки, которая задается в файле свойств. Методы header и footer просто выписывают стандартные теги HTML.



Листинг 8.18. Методы errorMsg, Header и Footer (TheNewsServ.java)



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

private void header(PrintWriter out ){ out.println("<html>"); out.println("<head><title>Company News Servlet</title> </head>"); out.println("<body>"); }

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

}






Корневой элемент документа


Мы отслеживаем некоторые параметры, используемые во всем файле, с помощью атрибутов, которые задаются в корневом элементе документа, Newsfile. Очередной атрибут id — это просто next id. Присваивание нового значения каждому следующему атрибуту nextid является обязанностью программы, добавляющей новые сообщения или, в случае редактирования в автономном режиме, автора элемента Newsitem.

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

В листинге 8.2 показано, как используются все перечисленные теги.

Листинг 8.2. Элемент <Newsfile> с одним элементом <Newsitem> (thenews.xml)

<?xral version="l.О" standalone="yes" ?>

<!-выходные данные NewsllpKeep -->

<Newsfile longtemplate="tmlong.html" nextid="1010">

<Newsitem timestamp="11045" topic="CDs" author="wbrogden" id="1008" >

<head>Your Favorite Music Now Available</head>

<date>Austin, Feb 1, 2000</date>

<short>

<![CDATA[XMLGifts proudly announces the availability of the

CD that has all the geeks singing,

<i>It's Dot Com Enough for He.</i>]]>

</short>

<long>

<![CDATA[<p>

<i>It's Dot Com Enough For He.</i>

now in stock!</p>

<p>All those great songs created during breaks in all-night coding sessions - now recorded by top Silicon Valley garage bands on our private label. <i>It's Dot Com Enough for Me</i> will have you singing along - or maybe laughing till the Jolt cola spurts out your nose. Seventeen songs from geeks at Sun. Microsoft, Apple. Cisco, and other top tech outfits. </P>]]>

</long>

</Newsitem>

</Newsfile>



Минимальная нагрузка на сервер


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

Статические страницы новостей (Static News Pages). Статические страницы быстро загружаются, и главная страница благодаря XML может формироваться заново при появлении нового сообщения. Однако использование статических страниц исключает возможность их индивидуальной настройки для постоянных посетителей.

Страницы новостей, генерируемые сервлетами (Servlet-Generated News Pages). Сервлеты Java могут генерировать все, что потребуется; при этом предполагается, что они используют файлы с шаблонами HTML для регулировки многочисленных атрибутов внешнего вида сайта.

JSP-страницы (JavaServer Pages). Преимущество JSP-страниц перед сервлетами заключается в том, что для изменения внешнего вида страницы web-дизайнеру не нужно уметь программировать на Java.

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

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



Пример JSP-страницы


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

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

Листинг 8.19. Первая часть упрощенной JSP-страницы для отображения новостей (mockup.jsp)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>The XMLGifts News </title> </head>

<body bgcolor="#FFFFFF"> <%@ page language="java" import= "com.XmlEcomBook.Chap08.NewsFormatter,java.io.*" %> <%! String newsFilePath = "e:\\scripts\\CompanyNews" ; String newsFileName = "thenews.xml" ; String newsHandler = "http://localhost:8080/XMLbook/Chap08/thenews.jsp" ; File newsFile = new File( newsFilePath, newsFileName ); public void jspInit(){ super.jspInit(); NewsFormatter.setHandler( newsHandler ); } %>

<table width="89%" border="0" align="left" cellpadding="8"> <tr align="center" bgcolor="cyan"> <td colspan="3"><font size="4"> Various Corporate Navigation Links Go Here</font> </td> </tr>

Чтобы не усложнять пример, мы жестко запрограммировали тему сообщений — музыкальные компакт-диски (листинг 8 20) Первый раз объект NewsFormatter используется для создания левого столбца таблицы, где расположены заголовки сообщений. Это делается в первую очередь, так как, когда тематика сообщений задана, объект NewsFormatter будет содержать только данные по сообщениям, соответствующим выбранной тематике

Листинг 8.20. Продолжение JSP-страницы с выходными данными NewsFormatter (mockup.java)


<!-- the nf and pw objects will be used for all three td --> <tr valign="TOP" ><font size="3"> <td><b>News Headlines</b><br> <% // topic could be set from customer records or the previous form String topic = "CDs" ; NewsFormatter nf = new NewsFormatter( newsFile ); PrintWriter pw = new PrintWriter( out );
/* Note the doNews signature doNews( PrintWriter out, String hs, String fs, String topstr, String sz, String age, int skpN, int mxN ) */ // headlines - all topics nf.doNews( pw, "","", "", "H", null, 0, 8 ); %>
</td> <td width="50%"> <% nf.doNews( pw, "","", topic, "L", null, 0, 1 ); %>
</td>
<!-- the short form column --> <td width="23%">
<%= "<b>Recent news items about " + topic + "</b><br>" %>
<%
/* Note the doNews signature doNews( PrintWriter out, String hs, String fs, String topstr, String sz, String age, int skpN, int mxN ) */
nf.doNews( pw, "","", topic, "S", null, 1, 8 ); %>
</td> </font> </tr> <tr align="center" bgcolor="cyan"> <td colspan="3"><font size="4" > Repeat the Navigation links here for convenience<br></font> </td> </tr> <tr> <td colspan="3" align='center'> <font face='arial, helvetica' size='3'> &copy;2000 XMLGifts.com<sup>SM</sup> <br /></font> </td> </tr> </table> </body> </html>
 


Простота ввода данных


Поскольку мы выбрали простой формат новостей, показанный в листинге 8.2, ввод данных осуществляется тоже достаточно просто. Система, основанная на формах HTML и сервлетах, описанная в разделе «Добавление свежих новостей» этой главы, позволяет добавлять новые сообщения через Интернет.

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

 



Расположение сообщений в зависимости от их новизны


Самые свежие новости должны располагаться первыми. Так как документ XML автоматически сохраняет порядок следования элементов, новые элементы должны добавляться к началу документа. Более того, было бы неплохо предусмотреть возможность отображения только самых свежих новостей. Следовательно, нам нужен способ представления «возраста» сообщений.

После долгих колебаний между многочисленными способами представления даты, которые позволили бы нам отображать только недавние сообщения, мы остановились на использовании простого целочисленного представления количества дней, прошедших с 1 января 1970 года. Для этого значение типа long, возвращаемое методом System.currentTimeMillisO, делится на количество миллисекунд в сутках, и полученное число становится значением атрибута timestamp тега News item. Альтернативные варианты — использование классов Java DateFormat или Calendar — были отвергнуты, так как они подразумевают создание большого количества объектов, а мы хотим, чтобы показ новостей создавал минимальную нагрузку на сервер.



Разработка системы показа новостей


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

Если вы подумаете, какими характеристиками должна обладать система показа новостей, в ваш список, вероятно, войдут следующие пункты:

гибкий способ отображения;

удобный ввод данных;

минимальная нагрузка на сервер.

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

 



Сервлет CompanyNewsServ


Форма HTML для обновления страницы новостей создается и управляется сер- влетом CompanyNewsServl et. Начальный вход в сервлет осуществляется с помощью HTML-страницы, в которой имеется обычная форма HTML для ввода имени автора и пароля. Пример такой страницы представлен в файле CoNewsUpdate.html, который находится на прилагаемом к книге компакт-диске. Сервлет отыскивает имя автора в файле свойств, проверяя таким образом, что этот человек имеет право на добавление новых сообщений.

В листинге 8.21 показан файл свойств для работы на сервере local host. Заметим, что имя автора является именем свойства, а пароль — его значением.

Листинг 8.21. Файл свойств, используемый сервлетом CompanyNewsServ (conewserv. properties)

# properties for CompanyNewsServ handler=http://localhost/servlet/conewserv thenewshandler=http://localhost/servlet/thenews newsfile=thenews.xml version=June 15, 2000 wbrogden=xmlrules

В листинге 8.22 показаны инструкции импорта, статические переменные и метод init для сервлета CompanyNewsServ.

Листинг 8.22. Начало кода метода CompanyNewsServ (ComanyNewsServ.java)

package com.XmlEcomBook.Chap08 ;

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

public class CompanyNewsServ extends HttpServlet { static String workDir = "E:\\scripts\\CompanyNews" ; static String propfile = "conewserv.properties" ; static String newsFile = "thenews.xml" ; static String handler = "http://localhost/servlet/conewserv" ; static String version = "v0.12"; static String pversion = "" ; static Properties cnProp ; static String brcrlf = "<br />\r\n" ;

public void init(ServletConfig config) throws ServletException { super.init(config); String tmp = config.getInitParameter("workdir"); if( tmp != null ) workDir = tmp ; tmp = config.getInitParameter("propfile"); if( tmp != null ) propfile = tmp ; System.out.println("Start CompanyNewsServ using " + workDir ); File f = new File( workDir, propfile ); try { cnProp = new Properties(); cnProp.load( new FileInputStream(f) ); tmp = cnProp.getProperty("handler"); if( tmp != null ) handler = tmp ; tmp = cnProp.getProperty("newsfile"); if( tmp != null ) newsFile = tmp ; pversion = cnProp.getProperty("version"); System.out.println("Loaded properties for CompanyNewsServ: " + handler + " file:" + newsFile ); }catch(IOException e){ System.out.println("Error loading " + e ); }


}

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



Листинг 8.23. Метод doGet создает форму для ввода нового сообщения (CompanyNewsServ.java)

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String username = req.getParameter("username"); String password = req.getParameter("password"); String action = req.getParameter("action"); String tmp = cnProp.getProperty(username); boolean userok = false ; if( tmp != null ){ userok = tmp.equals( password ); } header( out ); if( userok ){ generateForm( out, username, password ); } else { out.println("<p>User: " + username + " password: " + password + " not found.</p>" ); } footer( out ); }

Заполненная форма посылается методу doPost. Как показано в листинге 8.24, различные текстовые элементы извлекаются и передаются объекту NewsllpKeep с помощью метода addltem.



Листинг 8.24. Метод doPost собирает данные из формы (CompanyNewsServ.java)

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = new PrintWriter(resp.getOutputStream()); String username = req.getParameter("username"); String password = req.getParameter("password"); String action = req.getParameter("action"); String head = req.getParameter("head"); String date = req.getParameter("date"); String topics = req.getParameter("topics"); String shrtStr = req.getParameter("short").trim(); String longStr = req.getParameter("long").trim(); File f = new File( workDir, newsFile ); try { NewsUpkeep nup = new NewsUpkeep( f ); nup.addItem( head, date, topics, username, shrtStr, longStr ); header( out ); out.println("NewsUpkeep is " + nup + "<br />"); footer( out ); } catch( Exception e){ errorMsg( out, "CompanyNewsServ.doPost ", e ); } }



Форма HTML для ввода новых сообщений создается методом generateForm, как показано в листинге 8.25. Заметим, что имя пользователя и пароль вставлены в форму в виде скрытых значений.



Листинг 8.25. Метод generateForm создает форму для ввода (Com.panyNewsServ.java)

private void generateForm( PrintWriter out, String name, String pw ){ out.println("<h2>Enter Company News Item Data</h2>"); out.println("<form method=\"POST\" action=\"" + handler + "\" >"); out.println("Headline - 80 char max<br />"); out.println("<input type=\"text\" maxlength=\"80\" size=\"60\"" + " name=\"head\" ><br />" ); out.println("Dated <br />"); out.println("<input type=\"text\" maxlength=\"50\" size=\"40\"" + " name=\"date\" value=\"" + new Date().toString() + "\" ><br />" ); out.println("Topics separated by commas - please stick to the official list.<br />"); out.println("<input type=\"text\" maxlength=\"80\" size=\"60\"" + " name=\"topics\" ><br />" ); out.println("Short version <br />"); out.println("<textarea cols=\"60\" rows=\"3\" name=\"short\" >"); out.println("</textarea><br />"); out.println("Long version <br />"); out.println("<textarea cols=\"60\" rows=\"10\" name=\"long\" >"); out.println("</textarea><br />"); out.println("<input type=\"hidden\" name=\"username\" value=\"" + name + "\"><br>" ); out.println("<input type=\"hidden\" name=\"password\" value=\"" + pw + "\" ><br>"); out.println("<input type=\"submit\" name=\"action\" value=\"Submit\" ><br />" ); out.println("</form></center>"); }



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



Листинг 8.26. Служебные методы в сервлете CompanyNewsServ (CompanyNewsServ.java)

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

private void header(PrintWriter out ){ out.println("<html>"); out.println("<head><title>Company News Servlet</title> </head>"); out.println("<body>"); }

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

}

 




Система показа новостей


Окончательный вариант устройства системы показа новостей изображается блок-схемой, приведенной на рис. 8.1. Обработка информации, происходящая на сервере, представлена в правой части блок-схемы, а обработка в автономном режиме — в левой части. Исходный файл XML может редактироваться как в режиме подключения к сети, так и автономно, но предпочтительным является режим подключения, так как он позволяет автоматически задавать атрибуты id и timestamp.

Рис. 8.1. Обработка сообщений

Объектная модель документа поддерживается в памяти с помощью класса DOMlibrary, который был описан в главе 7. Обновление исходного файла XML в режиме подключения выполняется сервлетом CompanyNewsServ и классом NewsUpKeep, которые обсуждаются далее в этой главе. Формирование web-страниц новостей на основе текстов сообщений осуществляется сервлетами или JSP-страницами, использующими класс NewsFormatter, который обсуждается в следующем разделе. Для создания информационных бюллетеней и печатной версии требуются другие форматы, которые легко написать, основываясь на приведенных ниже примерах.

 



Внешний вид web-страницы


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

Рис. 8.2. Web-страница новостей, генерируемая с помощью JSP-страницы

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



Формат RSS


Компания Netscape первой предложила идею создания страниц новостей, поступающих из различных источников, на сайте Netscape Netcenter (www.netscape.com) и разработала специальный формат Rich Site Summary (RSS) для упрощения этого процесса. Идея состоит в том, что новости, размещенные на сотрудничающих с Netscape Netcenter web-сайтах, записываются в специальном едином для всех сайтов формате и автоматически становятся доступными на сайте Netscape Netcenter. Такой подход сделал сайт Netscape одним из наиболее популярных порталов в Интернете.

Основанный на XML формат RSS (иногда называемый также RDF Site Summary — Resource Description Framework Site Summary, стандарт на описание ресурсов) до сих пор является одним из основных форматов для распространения новостей в Интернете. Тот сайт, где фактически создается сообщение, формирует файл ресурса (feed) в формате RSS, определяя канал новостей (news channel). Этот файл должен быть выложен на открытый web-сайт, где он автоматически становится доступным для других сайтов, с которыми заключены соответствующие соглашения. Инструкции по созданию собственного канала новостей вы можете получить по адресу: http://my.netscape.com/publish/help/quickstart. Большой web-сайт, который каждый час обновляет заголовки новостей, поступающих по различным каналам, может оказаться очень привлекательным для многих посетителей. В качестве примера вы можете попробовать заглянуть на сайт http://my.userland.com (но будьте осторожны: вы можете провести там гораздо больше времени, чем планировали!).

Примером технически ориентированного сайта, подключенного к каналам RSS, является система Meerkat (www.oriellynet.com/meerkat).



Формат сообщений Moreover.com


Для задач, которые решаются в этой главе, мы используем формат импорта и экспорта сообщений, предложенный организацией Moreover.com (www.moreover.com). Мы выбрали Moreover.com для рассказа о технологиях обмена новостями по причине простоты используемого формата и большого количества тематических категорий на сайте. Разумеется, в пользу этого формата говорит и то, что он распространяется свободно.

Хотя организация Moreover.com была создана только в декабре 1999, к июлю 2000 она получала заголовки с 1500 сайтов. Программное обеспечение, занимающееся сбором заголовков новостей, автоматически разделяет их на более чем 250 категорий.

Формат, используемый Moreover.com, проще RSS и выдает только сами заголовки. Для индивидуальной настройки импорта информационных сообщений нужно зарегистрироваться в системе, сообщив свой электронный адрес и пароль, а затем выбрать нужные тематические категории и указать количество заголовков сообщений, которые вы хотели бы увидеть. На рис. 9.1 показана небольшая часть страницы, на которой происходит выбор тематических категорий.

Рис. 9.1. Выбор категорий заголовков на сайте Moreover.com



Главный управляющий класс


Теперь мы переходим к рассмотрению класса, который управляет процессом получения заголовков через определенные промежутки времени и обеспечивает доступ к ним сервлетов. Класс NetNewsSuper устроен согласно обычной схеме единичного класса (singleton). В нем имеются статические переменные и методы, которые гарантируют, что для каждого источника XML (URL-адрес вместе с именами папки и файла) создается только один экземпляр NetNewsSuper. Как показано в листинге 9.19, этот URL-адрес используется в качестве ключа для получения экземпляра NetNewsSuper из хэш-таблицы nnsHash или для его создания, если он еще не существует.

Листинг 9.19. Начало класса NetNewsSuper (NetNewsSuper.java) package com.XmlEcomBook.Chap09;

package com.XmlEcomBook.Chap09;

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

public class NetNewsSuper extends java.lang.Thread { static Hashtable nnsHash = new Hashtable() ; static long classLoaded = System.currentTimeMillis(); static long longTime = 1000 * 60 * 60 * 2 ;// two hours static int maxErrCt = 10 ; // source is URL + query, dest is abs //file path, destFname = name // hash stored by complete source string as key static synchronized NetNewsSuper getNetNewsSuper(String source, String destPth, String destFname ){ NetNewsSuper nns = (NetNewsSuper)nnsHash.get( source ); if( nns == null ){ nns = new NetNewsSuper( source, destPth, destFname ) ; nnsHash.put( source, nns ); } return nns ; }

static synchronized void removeNetNewsSuper( String sourceURL ){ Object obj = nnsHash.remove( sourceURL ); if( obj == null ){ System.out.println("removeNetNewsSuper of " + sourceURL + " failed." ); } else { // allow Thread to die ((NetNewsSuper)obj).running = false ; } }

Причина, по который мы сделали класс NetNewsSuper расширением класса Thread, вместо того чтобы реализовать интерфейс Runnable, связана с удобством отладки и управления набором сервлетов. Самое простое — это написать сервлет, который перечисляет все работающие потоки (объекты Thread) в виртуальной машине процессора сервлетов. Вы можете отобразить и имя экземпляра, и результат вызова метода toString.


Как видно из кода конструктора класса, приведенного в листинге 9.20, имя потока устроено таким образом, что включает в себя имя извлекаемого файла. Когда вызывается .конструктор, ему передаются URL-адрес источника, имя папки и имя файла, которые используются для записи данных XML. Поскольку предполагается, что получение нового набора заголовков будет происходить в фоновом режиме, то данный поток получает минимальный приоритет.



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

// instance variables and methods follow String sourceURL ; String destPath, destFname ; public String errStr ; public boolean usable ; int errCt = 0 ; boolean running ;

NewsModel newsM ; private NetNewsSuper (String source, String dest, String fname ){ sourceURL = source ; destPath = dest ; destFname = fname ; setName("NetNewsSuper " + fname ); setPriority( Thread.MIN_PRIORITY ); start(); System.out.println("NetNewsSuper Thread started"); }

Как показано в листинге 9.21, первое, что делает метод run, — вызывает метод checkSrc, который проверяет, существует ли уже требуемый файл XML. Если это не так, создается объект класса XMLgrabber и выполняется его метод doQueryNow для получения исходного файла XML. Если файл XML присутствует, то выполняется метод createModel, который создает новый объект NewsModel.



Листинг 9.21. Метод run класса NetNewsSuper (NetNewsSuper.java)

// low priority - check for need to update xml public void run(){ running = true ; try { // runs when first started if( !checkSrc() ){ XMLgrabber grab = new XMLgrabber ( sourceURL, destPath, destFname ); System.out.println("NetNewsSuper runs doQueryNow"); if( !grab.doQueryNow() ){ errCt++ ; System.out.println ("NetNewsSuper.run - bad return from grab"); } } createModel(); }catch(Exception e1){ errCt++ ; } while( running ){ try { sleep( longTime ); XMLgrabber grab = new XMLgrabber ( sourceURL, destPath, destFname ); //System.out.println("NetNewsSuper.run runs doQueryNow"); if( running && grab.doQueryNow() ){ if( errCt > 0 ) errCt-- ; createModel(); } else { errCt++ ; System.out.println ("NetNewsSuper.run - bad return from grab"); } }catch(InterruptedException ie){ errCt++ ; System.err.println("NetNewsSuper.run " + ie ); }catch(Exception ee ){ errCt++ ; System.err.println("NetNewsSuper.run " + ee ); } if( errCt > maxErrCt ){ System.out.println ("NetNewsSuper.run too many errors: " + errCt +" run exiting."); running = false ; } } System.out.println("Leaving NetNewsSuper.run method"); }



// return true if XML source file is found private boolean checkSrc(){ File f = new File( destPath, destFname ); return (f.exists() && f.canRead()); }

В листинге 9.22 показан метод, который создает новый экземпляр класса NewsModel и затем вызывает методы экземпляра loadXML и locateCategories. В случае ошибки переменная usabl е устанавливается равной fal se.



Листинг 9.22. Этот метод создает новый объект NewsModel (NetNewsSuper.java)

// xml source known to exist, go for it private synchronized void createModel(){ newsM = new NewsModel( destPath, destFname ); if( !newsM.loadXML()){ // error in getting data errStr = newsM.lastErr ; usable = false ; } else { newsM.locateCategories(); usable = true ; } }

Как мы увидим при обсуждении в следующем разделе классов NetNewsBean и NetNewsServ и как показано в листинге 9.23, сервлет запрашивает текущую модель NewsModel с помощью метода getNewsModel. Если она отсутствует, что может случиться из-за сбоя в сети, который прерывает нормальную работу метода run, метод getNewsModel делает попытку получить NewsModel заново.



Листинг 9.23. Метод getNewsModel возвращает NewsModel (NetNewsSuper.java)

// Note that there are two steps to getting a news //model resident: // 1. grabbing the current XML to local file if not there already // 2. creating the NewsModel from the local XML public synchronized NewsModel getNewsModel() throws Exception { if( newsM != null ) return newsM ; // must be newly created NetNewsSuper if( !checkSrc() ){ XMLgrabber grab = new XMLgrabber( sourceURL, destPath, destFname ); //System.out.println("getNewsModel runs doQueryNow"); if( !grab.doQueryNow() ){ // System.out.println(" bad return from grab"); return null ; } } // source exists, create model createModel(); return newsM ; // may or may not be usable }

Метод toString, как показано в листинге 9.24, предоставляет краткую сводку о текущем состоянии объекта NetNewSuper.

Листинг 9.24. Метод toString (NetNewsSuper.java)

public String toString() { StringBuffer sb = new StringBuffer( "NetNewsSuper for "); sb.append( sourceURL ); if( newsM == null ){ sb.append(" No NewsModel resident "); } else { sb.append(" NewsModel resident, status: " + usable ); } sb.append(" class loaded: " ); sb.append( new Date( classLoaded ).toString() ); return sb.toString() ; }

}

 




Источники новостей и стандарты


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

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

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

 



Класс NetNewsBean


Этот класс выполняет роль интерфейса между сервлетом и хранящимся в памяти объектом NewsModel, который соответствует конкретному источнику сообщений. Как показано в листинге 9.30, конструктор использует класс NetNewsSuper для получения текущего объекта NewsModel для заданного источника.

Листинг 9.30. Начало класса NetNewsBean (NetNewsBean.java) package com.XmlEcomBook.Chap09;

package com.XmlEcomBook.Chap09;

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

public class NetNewsBean { static String noDataStr ="No Data is available"; static String dataSourceErr = "Error when loading data " ; NewsModel newsM ; // has public boolean usable and errStr

// create with source url string, dest file path, dest fname NetNewsBean( String source, String pth, String fn ) throws Exception { NetNewsSuper nns = NetNewsSuper.getNetNewsSuper( source,pth,fn ); newsM = nns.getNewsModel() ; // throws exception }

public String getDocDate(){ if( newsM == null ) return noDataStr ; if( newsM.usable ) return newsM.dateStr ; return dataSourceErr ; }

В листинге 9.31 показаны методы, которые обеспечивают доступ к списку тематических категорий в конкретном объекте NewsModel. Метод getTopicsAsArray просто возвращает массив типа String, в то время как getTopicsAsSelect возвращает список в формате HTML.

Листинг 9.31. Методы, которые возвращают тематические категории в виде массива и в виде списка в формате HTML (NetNewsBean.java)

public String[] getTopicsAsArray(){ if( newsM == null || !newsM.usable ) return null; return newsM.getTopics(); } // return available topics as a Select control with values // matching the index of the topics array public String getTopicsAsSelect(){ if( newsM == null ) return noDataStr ; StringBuffer sb = new StringBuffer(1000); if( newsM.usable ){ String[] topics = newsM.getTopics(); sb.append(" <select name=\"topics\" MULTIPLE size=\"3\">\r\n"); for( int i = 0 ; i < topics.length ; i++ ){ sb.append("<option value=\""); sb.append( Integer.toString( i )); sb.append("\" > "); sb.append( topics[i] ); } sb.append("</select>\r\n"); } else { sb.append( dataSourceErr ); sb.append( newsM.lastErr ); } return sb.toString(); }


Метод getContentByKeyWord, показанный в листинге 9.32, контролирует выбор и формат заголовков, содержащих одно или несколько ключевых слов, введенных пользователем.



Листинг 9.32. Метод, контролирующий поиск заголовков по ключевым словам (NetNewsBean.java)

public String getContentByKeyWord( String kwds, String fmt ){ if( newsM == null ) return noDataStr ; StringBuffer sb = new StringBuffer(1000); if( newsM.usable ){ Element[] art = newsM.articlesByKeyWord( kwds ); for( int i = 0 ; i < art.length ; i++ ){ sb.append( newsM.formatElement( art[i], fmt )); sb.append("\n"); } } else { sb.append( dataSourceErr ); sb.append( newsM.lastErr ); } return sb.toString(); }

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

Листинг 9.33. Метод getAHTopics форматирует все имеющиеся заголовки (NetNewsBean.java)

public String getAllTopics( String fmt ){ if( newsM == null ) return noDataStr ; StringBuffer sb = new StringBuffer(1000); if( newsM.usable ){ Element[] art = newsM.getAllTopics(); for( int i = 0 ; i < art.length ; i++ ){ sb.append( newsM.formatElement( art[i], fmt )); sb.append("\n"); } } else { sb.append( dataSourceErr ); sb.append( newsM.lastErr ); } return sb.toString(); }

На рис 9 3 показана страница с заголовками свежих новостей, выбранных в соответствии с указанными ключевыми словами и форматированных методом getContentByTopic, который приведен в листинге 934





Рис. 9.3. Представление заголовков сообщений



Листинг 9.34. Метод getContentByTopic (NetNewsBean.java)

public String getContentByTopic( String content, String fmt ){ if( newsM == null ) return noDataStr ; StringBuffer sb = new StringBuffer(1000); if( newsM.usable ){ Element[] art = newsM.articlesByTopic( content ); if( art == null ) return dataSourceErr ; for( int i = 0 ; i < art.length ; i++ ){ sb.append( newsM.formatElement( art[i], fmt )); sb.append("\n"); } } else { sb.append("getContentByTopic " + dataSourceErr ); sb.append("getContentByTopic " + newsM.lastErr ); } return sb.toString(); }

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

}






Класс NewsModel


Теперь, когда файл XML и соответствующий файл DTD находятся на локальном жестком диске, нам нужен класс для синтаксического анализа файла XML и создания DOM. Написанный нами для ^того класс называется NewsModel; он создает коллекции элементов в соответствии с типом содержимого, или, если использовать терминологию Moreover.com, кластеры (clusters). В классе NewsModel предусмотрены также методы для извлечения и форматирования отдельных элементов.



Классы для отображения заголовков


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



NewsML и планы стандартизации


Международный совет по медиа-телекоммуникациям (The International Press Telecommunications Council, IPTC), представленный по адресу www.iptc.org, предпринимает попытки создать на основе XML стандарт кодирования, который упростил бы процесс составления, передачи и получения информационных сообщений. Во время написания этой книги была выпущена версия 1.0 этого стандарта, названного NewsML.

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

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

 



Получение файла XML


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

www.moreover.com/cgi-local/page?wbrogden@bga.com+xml

После получения доступа к этому ресурсу на сервере Moreover.com специальное приложение возвращает текстовый поток в формате XML, содержащий заголовки по выбранным темам. В листинге 9.1 показаны заголовок и первый элемент в том виде, в котором они были исходно получены при загрузке файла XML. Обратите внимание на то, что строка DOCTYPE ссылается на определение DTD, расположенное на сайте Moreover.com. Чтобы обеспечить возможность анализа файла XML во время тестирования без доступа к web-сайту Moreover.com, класс XMLgrabber модифицирует строку DOCTYPE так, чтобы она ссылалась на определение DTD как на локальный файл.

Листинг 9.1. Заголовок и первый элемент загруженного файла XML (xmldump.xml)1

<?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE moreovernews SYSTEM "moreovernews.dtd"> <moreovernews> <article id="_8510757"> <url>http://c.moreover.com/click/here.pl?x8510756</url> <headline_text>Cyclone Commerce Poised to Fulfill Promise of E-Signature Legislation</headline_text> <source>Java Industry Connection</source> <media_type>text</media_type> <cluster>Java news</cluster> <tagline> </tagline> <document_url> http://industry.java.sun.com/javanews/more/hotnews/ </document_url> <harvest_time>Jul 25 2000 8:34AM</harvest_time> <access_registration> </access_registration> <access_status> </access_status> </article>

Обратите внимание, что элемент, который в Moreover.com называется cl uster, идентифицирует основную тематическую категорию, к которой относится данное сообщение. В приведенном ниже примере сервлета мы используем только элементы url, headline_text, source и cluster. Определение DTD moreovernews, как видно из листинга 9.2, устроено относительно просто.




Листинг 9.2. Файл moreovernews.dtd (moreovernews.dtd)

<!ELEMENT moreovernews (article*)> <!ELEMENT article (url,headline_text,source,media_type, cluster,tagline,document_url,harvest_time, access_registration,access_status)> <!ATTLIST article id ID #IMPLIED> <!ELEMENT url (#PCDATA)> <!ELEMENT headline_text (#PCDATA)> <!ELEMENT source (#PCDATA)> <!ELEMENT media_type (#PCDATA)> <!ELEMENT cluster (#PCDATA)> <!ELEMENT tagline (#PCDATA)> <!ELEMENT document_url (#PCDATA)> <!ELEMENT harvest_time (#PCDATA)> <!ELEMENT access_registration (#PCDATA)> <!ELEMENT access_status (#PCDATA)>

Сервлет, который используется в рассматриваемом в этой главе приложении, включает в себя класс NetNewsSuper. Этот класс запускает поток Thread, который создает объект XMLgrabber для загрузки файла с наиболее свежими заголовками новостей, а также текущий файл DTD. В листинге 9.3 показано начало этого класса, в том числе его конструктор. Заметим, что конструктор снабжен URL- адресом искомого ресурса, а также содержит путь и имя файла, который будет использоваться для локальной копии.

Листинг 9.3. Начало класса XMLgrabber (XMLgrabber.java)

package com.XmlEcomBook.Chap09;

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

public class XMLgrabber { String source ; // complete URL to run std query // example "http://www.moreover.com/cgi-local/ page?wbrogden@bga.com+xml"; String saveDir ; // for both temp and final xml file String tfnxml ; // temp file name - see createTempXmlWriter String tfndtd ; // temp file name for dtd String saveName ; // for xml String dtdURL ; // complete DTD url from <!DOCTYPE line String dtdFname ; // for dtd

PrintWriter pw ; URL theURL ; Thread queryT ;

// all files from a given source will go to dest directory XMLgrabber( String src, String dest, String fname ){ source = src ; saveDir = dest ; saveName = fname ; // System.out.println("XMLgrabber initialized for " + src ); }



Поскольку мы не можем всегда рассчитывать на быстрое соединение с сайтом Moreover.com и так как в некоторых случаях попытка получить новые данные может провалиться, получение данных XML осуществляется отдельным от функций сервлета потоком (объектом Thread). Более того, файл со старыми данными не заменяется сразу же новым файлом; вместо этого полученные данные записываются во временный файл. Только когда мы удостоверимся, что полученный файл XML содержит все необходимые данные, мы удаляем старый файл и присваиваем имя временному файлу. Как показано в листинге 9.4, метод doQueryNow создает структуру для получения наиболее свежей информации. Помимо получения файла XML, содержащего заголовки сообщений, мы также пытаемся получить самое последнее определение DTD.



Листинг 9.4. Метод doQueryNow (XMLgrabber.java)

// run by external Thread to get file resident // return true if suceeds public boolean doQueryNow() throws IOException { theURL = new URL( source ); createTempXmlWriter(); grabXml(); if( !renameTemp( tfnxml, saveName )) return false ; tfnxml = null ; // now for the dtd if( dtdURL == null ) return true ; // System.out.println("Start DTD retrieval"); theURL = new URL( dtdURL ); createTempDtdWriter(); grabDtd(); boolean ret = renameTemp( tfndtd,dtdFname ); tfndtd = null ; return ret ; }

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

Файл tfnxml создается при вызове метода createTempXmlWriter. Заметим, что если не возникнет исключительных ситуаций и переименование файла tfnxml в saveName пройдет успешно, переменная tfnxml устанавливается равной null. В противном случае метод finalize (листинг 9.9) попытается удалить временный файл. Файл tfndtd, предназначенный для загрузки DTD, проходит такую же обработку.



В листинге 9. 5 показаны методы объекта XMLgrabber, которые применяются для создания и управления временными файлами.



Листинг 9.5. Методы для управления временными файлами (XMLgrabber.java)

// saveDir used for all private boolean renameTemp(String tmp, String saveN ) { File src = new File( saveDir, tmp ); File dest = new File( saveDir, saveN); dest.delete(); return src.renameTo( dest ); }

private void createTempXmlWriter() throws IOException { tfnxml = "$" + Integer.toString((int) System.currentTimeMillis()) + ".xml"; File tfile = new File( saveDir, tfnxml ); while( tfile.exists() ){ // hunt for unique name tfnxml = tfnxml + "X" ; tfile = new File( saveDir, tfnxml ); } // ok, unique file name in tfnxml, File tfile set up FileWriter fw = new FileWriter( tfile.getAbsolutePath() ); pw = new PrintWriter( fw ); }

private void createTempDtdWriter() throws IOException { tfndtd = "$" + Integer.toString((int) System.currentTimeMillis()) + ".dtd"; File tfile = new File( saveDir, tfndtd ); while( tfile.exists() ){ // hunt for unique name tfndtd = tfndtd + "X" ; tfile = new File( saveDir, tfndtd ); } // ok, unique file name in tfndtd, File tfile set up FileWriter fw = new FileWriter( tfile.getAbsolutePath() ); pw = new PrintWriter( fw ); }

Как показано в листинге 9.6, метод grabXml считывает строки текста из входного потока, создав соединение по нужному URL-адресу. Строка заголовка XML, содержащая ссылку на файл DTD, переформатируется методом reformDoctype, который заменяет URL на имя локального файла. Это делается для того, чтобы гарантировать, что синтаксический анализ файла XML будет проходить независимо от соединения с Интернетом.



Листинг 9.6. Метод grabXML считывает строки файла XML из заданного с помощью URL источника (XMLgrabber.java)

// at this point pw is open to a temp file private void grabXml() throws IOException { URLConnection urlC = theURL.openConnection(); urlC.setUseCaches( false ); urlC.setAllowUserInteraction(false); urlC.connect(); InputStream is = urlC.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader( isr ); String tmp = br.readLine() ; while( tmp != null ){ tmp = tmp.trim(); if( tmp.startsWith("<!DOCTYPE") ) { // change to use local copy tmp = reformDoctype( tmp ); } pw.println( tmp ); tmp = br.readLine(); } pw.close(); // does a flush() }



В предположении, что метод ref ormDoctype корректно задает переменную dtdURL, метод grabDtd, показанный в листинге 9.7, загружает DTD в локальный временный файл. В этом листинге также показан служебный метод createURL, который устанавливает значение переменной экземпляра theURL.



Листинг 9.7. Метод grabDtd получает текущее определение moreovernews.dtd (XMLgrabber.java)

// at this point pw is open to a temp file for dtd private void grabDtd() throws IOException { System.out.println("grabDtd:" + dtdURL ); URLConnection urlC = theURL.openConnection(); urlC.setUseCaches( false ); urlC.setAllowUserInteraction(false); urlC.connect(); InputStream is = urlC.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader( isr ); String tmp = br.readLine() ; while( tmp != null ){ pw.println( tmp.trim() ); tmp = br.readLine(); } pw.close(); // does a flush() //System.out.println("grabDtd OK"); }

private boolean createURL(String str){ try { theURL = new URL( str ); return true ; }catch(MalformedURLException e){ return false ; } }

Как показано в листинге 9.8, метод reformDoctype извлекает ссылку на файл DTD из строки DOCTYPE, а затем устанавливает значения переменных dtdURL и dtdFname.



Листинг 9.8. Метод reformDocType модифицирует ссылку на DTD (XMLgrabber.java)

// string has doctype declaration, revise to //point to local version private String reformDoctype( String dts ){ int p1 = dts.indexOf( "http:"); // points at h if( p1 < 0 ) return dts ; // int p2 = dts.indexOf( '"', p1 ); int p3 = dts.lastIndexOf('/', p2); if( p3 < 0 ) return dts ; dtdURL = dts.substring( p1 , p2 ); dtdFname = dts.substring( p3 + 1, p2 ); // System.out.println("DTD url:" + dtdURL + "<"); // System.out.println("DTD fname:" + dtdFname + "<"); String tmp = dts.substring(0,p1); // includes " return tmp + dts.substring( p3 + 1 ); }

Так как процесс загрузки в некоторых точках может быть прерван, существует опасность накопления пустых или частично записанных временных файлов. Метод finalize, как показано в листинге 9.9, пытается удалить эти файлы, если они существуют. Если все идет нормально, переменные tfnxml и tfndtd должны содержать null и тогда попытки удалить временные файлы не происходит.



Листинг 9.9. Метод finalize может удалять временные файлы (XMLgrabber.java)

// last chance to clean up temp files if something failed public void finalize(){ if( tfnxml != null ){ new File( saveDir, tfnxml ).delete(); } if( tfndtd != null ){ new File( saveDir, tfndtd ).delete(); } } }

 




Сервлет NetNewsServ


Этот сервлет выполняет две существенные функции: метод doGet создает форму, которая позволяет пользователю выбирать интересующие его темы сообщений и/или задавать ключевые слова, а метод doPost осуществляет сам процесс отображения подходящих заголовков. В листинге 9.25 показано начало кода серв- лета. Чтобы не усложнять наш пример, мы жестко запрограммировали значение переменной queryStr, которая содержит URL для поиска соответствующего ресурса, путь и имя файла XML, а также значение переменной alias с URL-адре- сом сервлета. В реально работающей системе эти переменные будут считываться из файла свойств в методе init.

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

package com.XmlEcomBook.Chap09;

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

public class NetNewsServ extends HttpServlet { static String version = "1.02 July 26, 2000"; static String queryStr = "http://www.moreover.com/cgi-local/page" + "?wbrogden@bga.com+xml"; static String destDir = "e:\\scripts\\netnews" ; static String queryFile = "xmldump.xml" ; static String alias = "http://www.lanw.com/servlet/netnews" ;

String keywords = "Amazon,Dell,Microsoft"; String fmt = "<tr><td><a href=\"<%url>\" > <%headline_text></a>" + " &nbsp; from <%source></td></tr>" ;

public void init(ServletConfig config) throws ServletException { super.init(config); }

Метод doGet генерирует простую форму, которая позволяет выбрать одну или несколько тематических категорий и/или ввести ключевые слова. Как показано в листинге 9.26, он получает объект NetNewsBean для определенного источника новостей. Метод getTopicsAsSelect объекта NetNewsBean создает код для отображения списка возможных категорий. Получившаяся в результате страница HTML показана на рис. 9.2.

Листинг 9.26. Метод doGet создает простую форму (NetNewsServ.java)


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

out.println("<HTML>"); out.println("<HEAD><TITLE>NetNewsServ Output</TITLE> </HEAD>"); out.println("<BODY>"); try { NetNewsBean nnb = new NetNewsBean( queryStr, destDir, queryFile );

out.println("<h1>The News</h1>"); out.println("<p>Select the general categories you would like to see. "+ "You can also enter a list of key words or phrases separated by " + "commas and the system will locate any headlines containing them.</p>" ); out.println("<center><form method=\"POST\" action=\"" + alias + "\" >" ); out.println("Key Words: <input type=\"TEXT\" size=\"60\"" + " maxlength=\"120\" name=\"keywords\" ><br>"); out.println("Select one or more topics (use &lt;ctrl&gt;click.) <br>"); out.println( nnb.getTopicsAsSelect() ); out.println("<br> <input type=\"SUBMIT\" value=\"Continue\" >"); out.println("</form></center><br>"); footer( out ); }catch(Exception e){ errorMsg( out, "NetNewsServ.doGet ", e ); } }





Рис. 9.2. Форма для выбора темы заголовков

Когда пользователь щелкает на кнопке Continue (Продолжить), методу doPost, приведенному в листинге 9.27, отправляется запрос. Если пользователь не ввел никаких ключевых слов и не выбрал никаких категорий в списке, вызывается метод doGet для обновления содержимого формы. В противном случае генерируется страница, содержащая таблицу HTML, строки которой создаются при помощи метода doNetNews. Разумеется, это сильно упрощенный вариант функционирования реального коммерческого сайта.





Листинг 9.27. Метод doPost (NetNewsServ.java)

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

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

}

Как показано в листинге 9.28, метод doNetNews получает объект NetNewsBean и выводит полученные заголовки в виде строк таблицы.



Листинг 9.28. Этот метод форматирует полученные заголовки (NetNewsServ.java)

// assumes a table has been started // topics[] are tags from list, ie 0, 1 etc. private void doNetNews ( PrintWriter out,String keywords, String[] topics ){ int i =0 ; try { NetNewsBean nnb = new NetNewsBean ( queryStr, destDir, queryFile ); out.println("Update " + nnb.getDocDate()); String[] tstr = nnb.getTopicsAsArray(); if( keywords.length() > 0 ) { out.println("<tr><td> Selected by keywords: " + keywords + "</td></tr>"); out.println( nnb.getContentByKeyWord( keywords, fmt )) ; } out.println("<hr>") ; if( topics == null ){ // none selected for( i = 0 ; i < tstr.length ; i++ ){ out.println("<tr><td>topic: " + tstr[i] + "</td></tr>" ); out.println( nnb.getContentByTopic( tstr[i], fmt ) ); } } else { for( i = 0 ; i < topics.length ; i++ ){ int tn = Integer.parseInt( topics[i] ); out.println("<tr><td>topic: " + tstr[tn] + "</td></tr>" ); out.println(nnb.getContentByTopic(tstr[ tn ], fmt)); } } }catch(Exception e){ out.println( "<tr><td>" ); e.printStackTrace(out ); out.println("</td></tr>"); } }



Все остальные методы в классе NetNewsServ являются служебными; некоторые из них показаны в листинге 9.29.



Листинг 9.29. Некоторые служебные методы (NetNewsServ.java)

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

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

}

 




Создание DOM


В листинге 9.10 показано начало класса NewsModel, включая переменные экземпляра и конструктор. Обратите внимание на то, что здесь присутствуют две коллекции, Hashtable и Nodelist, которые будут «заселены» элементами article, когда завершится создание DOM. Конструктору просто передаются параметры, содержащие путь и имя файла XML. В зависимости от того, какой анализатор XML вы используете, вам может потребоваться несколько изменить инструкции импорта и метод loadXML, показанный в листинге 9.11, но остальные методы должны работать с любым анализатором, потому что они используют интерфейсы org.w3c.dom.

Листинг 9.10. Начало класса NewsModel (NewsModel.java) package com.XmlEcomBook.Chap09;

package com.XmlEcomBook.Chap09;

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

public class NewsModel { long timestamp ; public String dateStr ; Document doc ; String path, fname ; public boolean usable ; public String lastErr ="no error"; // see locateCategories for creation of following Hashtable clusterHash ; NodeList articleNodeList ;

public NewsModel( String pth, String fn ){ path = pth ; fname = fn ; }

Процесс синтаксического анализа контролируется методом loadXML, как показано в листинге 9.11. Заметьте, что (для сохранения максимально полной информации по отладке) исключения SAXParseException перехватываются по отдельности и подробная информация о причинах возникновения исключения сохраняется в переменной lastErr. Хотя документы XML, содержащиеся на сайте Moveover.com, обычно правильно оформлены, у нас был один случай неправильно оформленного документа — в нем содержался недопустимый символ. В этом случае подробная информация о причинах исключительной ситуации при синтаксическом анализе оказалась очень ценной.

Листинг 9.11. Метод loadXML осуществляет синтаксический анализ (NewsModel.java)

// return true if sucessful - if false, see lastErr public synchronized boolean loadXML( ) { File xmlFile = new File( path, fname ); System.out.println("NewsModel.loadXML start " + xmlFile.getAbsolutePath() ); try { timestamp = xmlFile.lastModified(); dateStr = new Date( timestamp ).toString(); InputSource input = Resolver.createInputSource( xmlFile ); // ... the "false" flag says not to validate (faster) // XmlDocument is in the com.sun.xml.tree package doc = XmlDocument.createXmlDocument (input, false); System.out.println("Created document"); usable = true ; return true ; }catch(SAXParseException spe ){ StringBuffer sb = new StringBuffer( spe.toString() ); sb.append("\n Line number: " + spe.getLineNumber()); sb.append("\nColumn number: " + spe.getColumnNumber() ); sb.append("\n Public ID: " + spe.getPublicId() ); sb.append("\n System ID: " + spe.getSystemId() + "\n"); lastErr = sb.toString(); System.out.print( lastErr ); return false ; }catch( SAXException se ){ lastErr = se.toString(); System.out.println("loadXML threw " + lastErr ); se.printStackTrace( System.out ); return false ; }catch( IOException ie ){ lastErr = ie.toString(); System.out.println("loadXML threw " + lastErr + " trying to read " + xmlFile.getAbsolutePath() ); return false ; } } // end loadXML


Если анализ XML завершился успешно и была создана объектная модель документа, вызывается метод locateCategories. Как показано в листинге 9.12, этот метод получает NodeList — список всех элементов article, и записывает его в переменную articleNodeList. Затем он вызывает метод processArticle для каждого элемента. Метод processArticle строит вектор элементов для каждого значения элемента cluster. Это как раз тот вектор, который выдает заголовки сообщений, если в запросе пользователя указана конкретная тематика.



Листинг 9.12. Метод locateCategories классифицирует заголовки (newsModel.java)

public void locateCategories(){ Element dE = doc.getDocumentElement(); // the root element clusterHash = new Hashtable(); articleNodeList = dE.getElementsByTagName("article"); int act = articleNodeList.getLength(); //System.out.println("Article count: " + act ); for( int i = 0 ; i < act ; i++ ){ Element aE = (Element) articleNodeList.item( i ) ; processArticle( aE ); } }

private void processArticle( Element artE ){ NodeList clusterNL = artE.getElementsByTagName("cluster"); if( clusterNL.getLength() == 0 ) return ; Element clE = (Element)clusterNL.item(0); String clusterStr = clE.getFirstChild().getNodeValue().trim() ; // System.out.println("cluster ct " + clusterNL.getLength() + // " value: " + clusterStr ); Object obj = clusterHash.get( clusterStr ); Vector v = null ; if( obj == null ){ v = new Vector(); clusterHash.put( clusterStr, v ); } else {v = (Vector)obj ; } v.addElement( artE ); }

 




Возможные усовершенствования


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

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

Создание пользовательской библиотеки тегов JSP для заголовков не требует написания длинных программ, но такая библиотека упростила бы работу web- дизайнера по наделению любой страницы средствами представления заголовков новостей, причем без необходимости модифицировать код Java.



Выбор заголовков


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

При вызове метода articlesByKeyword, показанного в листинге 9.13, ему передается строка, содержащая одну или более последовательностей символов, разделенных запятыми. Первый шаг заключается в преобразовании этой строки keystring в массив типа String. Этот шаг выполняется методом prepKeys, который, кроме того, переводит все символы в верхний регистр. Далее метод searchArticle осуществляет поиск по всем элементам article и все совпадения возвращаются в виде массива ссылок на соответствующие элементы.

Листинг 9.13. Метод articlesByKeyWord вызывается сервлетом (NewsModel.java)

// articles by keyword appearance in headline // keys may be word or phrase, one or more, sep by comma // just use original order public Element[] articlesByKeyWord( String keystring ){ String[] keys = prepKeys( keystring ); // upper case and separated Vector v = new Vector(); int i ; int ct = articleNodeList.getLength(); for( i = 0 ; i < ct ; i++ ){ Element aE = (Element) articleNodeList.item( i ) ; if( searchArticle( aE, keys )){ v.addElement(aE); } } Element[] ret = new Element[ v.size() ]; for( i = 0 ; i < ret.length ; i++ ){ ret[i] = (Element) v.elementAt(i); } return ret ; } // convert to upper case and separate at commas private String[] prepKeys( String s ){ StringTokenizer st = new StringTokenizer( s.toUpperCase(), ","); String[] ret = new String[ st.countTokens() ]; int i = 0 ; while( st.hasMoreTokens() ){ ret[i++] = st.nextToken().trim(); } return ret ; }

Метод searchArticle, как показано в листинге 9.14, сложнее, чем вы, возможно, ожидали. Это объясняется некоторыми особенностями анализатора XML. Рассмотрим содержимое элемента <headline_text>, куда включена сущность (например, &атр;):


<headline_text>Q&amp;A: Will Sony I

Rule the Digital World</ I headline_text>

Эта строка будет разделена анализатором на три объекта Node: два текстовых узла, разделенных узлом EntityReference. Так как нам нужен полный текст заголовка, для получения соответствующей строки вызывается метод getFullText, выдающий текст заголовка целиком.

Метод getFullText, который также показан в листинге 9.14, объединяет текст всех частей заголовка. Текст, представляющий узел EntityReference, должен быть построен как объединение с символами ; и & имени узла.



Листинг 9.14. Методы, которые поддерживают поиск заголовков по ключевым словам (NewsModel.java)

// return true if one of the keys appears //in the headline_text element private boolean searchArticle( Element aE, String[] keys ){ NodeList htNL = aE.getElementsByTagName("headline_text"); if( htNL.getLength() == 0 ) return false ; // there is only one headline_text Element htE = (Element)htNL.item(0); String str = getFullText( htE ).toUpperCase() ; for( int i = 0 ; i < keys.length ; i++ ){ if( str.indexOf( keys[i] ) >= 0 ) return true ; } return false ; } // this is needed to cope with headline text that has entities private String getFullText( Node nd ){ NodeList nl = nd.getChildNodes(); int ct = nl.getLength(); if( ct == 0 ) return ""; if( ct == 1 ) return nd.getFirstChild().getNodeValue(); StringBuffer sb = new StringBuffer(); for( int i = 0 ; i < ct ; i++ ){ Node n = nl.item(i); if( n instanceof EntityReference ){ // reconstruct &amp; notation sb.append( '&' ); sb.append( n.getNodeName()); sb.append( ';' ); } else { sb.append( n.getNodeValue() ); } } return sb.toString(); }

Метод locateCategories (см. листинг 9.12) создал коллекцию clusterHash, в которой элементы (заголовки сообщений) хранятся в соответствии со своими тегами <cluster>. Мы не совсем понимаем, почему в Moreover.com этот элемент называется cluster (кластер) — нам кажется, что более подходящим названием для этого тега было бы topic (тема); поэтому метод, приведенный в листинге 9.15, называется articlesByTopic (статьи по темам). В этом листинге также показан метод getAllTopics, который просто превращает весь список узлов articleNodeList в массив типа Element.





Листинг 9.15. Этот метод возвращает все элементы с заданным значением элемента cluster (NewsModel.java)

// return array of Element for this topic // or null if none available public Element[] articlesByTopic( String topic ){ Vector v = (Vector) clusterHash.get( topic ); if( v == null ) return null ; Element[] ret = new Element[ v.size() ]; for( int i = 0 ; i < ret.length ; i++ ){ ret[i] = (Element) v.elementAt( i ); } return ret ; }

public Element[] getAllTopics(){ int ct = articleNodeList.getLength(); Element[] ret = new Element[ ct ]; for( int i = 0 ; i < ct ; i++ ){ ret[i] = (Element)articleNodeList.item( i ); } return ret ; }

Чтобы пользователь мог выбирать темы сообщений, нам нужно иметь список всех тем, имеющихся в текущей модели DOM. Для создания списка тематических категорий применяется класс NetNewsBean, который мы и будем рассматривать ближе к концу этой главы. В этом классе используется массив типа String, который создается в методе getTopics, приведенном в листинге 9.16. Обратите внимание на то, что для сортировки объектов Srting в данном массиве необходим метод shell Sort, потому что порядок следования ключей в хэш-таблице непредсказуем.



Листинг 9.16. Метод getTopics (NewsModel.java)

// return exact names of all topics available public String[] getTopics(){ Enumeration keys = clusterHash.keys(); String[] ret = new String[ clusterHash.size() ]; int i = 0; while( keys.hasMoreElements() ){ ret[i++] = (String)keys.nextElement(); } shellSort( ret ); return ret ; }

Метод formatElement, показанный в листинге 9.17, предлагает простой способ вставить текст из объекта art (который соответствует некоторому заголовку) в строку, обычно содержащую информацию относительно разметки HTML. Ниже приводится пример такой форматирующей строки, в которой содержатся теги, показывающие, куда нужно вставить элементы url, headline_text и source:

<tr><td><a href="<%url>"x%headline_text> </a> "&nbsp; from <%source></ tdx/tr>



Работа этого метода заключается в отыскании символов <%, выделении имени элемента и вызове метода getContent для извлечения текста элемента из соответствующего объекта Element. Заметим, что метод getContent вызывает метод getFullText для получения полного текста выбранного элемента.



Листинг 9.17. Метод formatElement (NewsModel.java)

// Element known to be an article, formatting string public String formatElement( Element art, String fmt ){ StringBuffer sb = new StringBuffer( 3 * fmt.length() ); int p0 = 0 ; int p1 = fmt.indexOf("<%"); int p2 = fmt.indexOf('>', p1); while( p1 > p0 && p2 > p1 ){ sb.append( fmt.substring( p0, p1 )); sb.append( getContent( art, fmt.substring(p1 + 2, p2) )); p0 = p2 + 1 ; p1 = fmt.indexOf("<%", p0); if( p1 > p0 ){ p2 = fmt.indexOf('>', p1); } } sb.append( fmt.substring( p0 )); return sb.toString(); } // element known to be an article private String getContent( Element art, String key ){ NodeList nl = art.getElementsByTagName( key ); if( nl.getLength() == 0 ) return ""; Element kE = (Element)nl.item(0); return getFullText( kE ) ; }

Последняя часть кода класса NewsModel, приведенная в листинге 9.18, содержит метод she! 1 Sort и некоторые другие служебные методы.



Листинг 9.18. Метод для сортировки и другие служебные методы (NewsModel.java)

public void shellSort (String[] srted ) { // h is the separation between items we compare. int h = 1; while ( h < srted.length ) { h = 3 * h + 1; } // now h is optimum while ( h > 0 ) { h = (h - 1)/3; for ( int i = h; i < srted.length; ++i ) { String item = srted[i]; int j=0; for ( j = i - h; j >= 0 && compare( srted[j], item ) < 0; j -= h ) { srted[j+h] = srted[j]; } // end inner for srted[j+h] = item; } // end outer for } // end while } // end sort

// return -1 if a < b , 0 if equal, +1 if a > b int compare(String a, String b ){ String aa = a.toUpperCase() ; String bb = b.toUpperCase() ; return bb.compareTo( aa ) ; }

public String toString() { StringBuffer sb = new StringBuffer( "NewsModel " ); if( !usable ){ sb.append("is not usable due to "); sb.append( lastErr ); return sb.toString(); } sb.append("count of articles "); sb.append( Integer.toString( articleNodeList.getLength()) ); sb.append("Unique clusters " + clusterHash.size() ); sb.append("\n"); Enumeration keys = clusterHash.keys(); while( keys.hasMoreElements() ){ String key = (String)keys.nextElement(); Vector v = (Vector)clusterHash.get( key ); sb.append(" Topic: " ); sb.append( key ) ; sb.append(" has " ) ; sb.append(Integer.toString( v.size())); sb.append("\n"); } return sb.toString(); } }






Альтернативное решение — Spaces


В отличие от жестко структурированной среды J2EE в концепции Spaces web- приложение создается как слабосвязанная система. Эта концепция несколько лет обсуждалась в академических кругах, а недавно было найдено новое ее применение. Проект JINI, разработанный Sun для связи между распределенными приложениями, использует новый интерфейс API JavaSpaces.

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

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

Тем, кто ищет подробную информацию о программировании в соответствии с концепцией пространства сообщений JavaSpace, можно предложить в качестве исходного пункта следующий адрес:

http://java.sun.com/products/javaspaces/



Архивные файлы web-приложения


Правила организации папок для web-приложений, заданные в API сервлетов, позволяют определить формат архивного файла, в котором будут содержаться все ресурсы, необходимые для работы приложения. Согласно терминологии Sun, такой формат называется WAR, или Web Application Archive (архив web-приложения). Фактически это такой же формат, как хорошо знакомый формат JAR. Идея заключается в том, что созданный поставщиком программного обеспечения WAR-файл, содержащий web-приложение, можно поместить в файловую систему любого подходящего сервера, и он автоматически распакуется, образуя определенную структуру папок приложения.

Эта автоматическая распаковка происходит тогда, когда при запуске сервера обнаруживается появление нового WAR-файла. За распаковку архива отвечает web-сервер.



Дескриптор развертывания web-приложения


В API сервлетов имеются спецификации, которые на основе составленного компанией Sun определения DTD идентифицируют сведения, содержащиеся в дескрипторе развертывания приложения. В листинге 10.1 показана часть файла web.xml, который использовался для некоторых сервлетов, описанных в этой книге.

Листинг 10.1. Часть файла web.xml для web-приложения (web.xml)

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc. //DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">

<web-app> <servlet><servlet-name>cattest</servlet-name> <servlet-class>com.XmlEcomBook.catalog.CatalogTestServ </servlet-class> </servlet> <servlet><servlet-name>catalog</servlet-name> <servlet-class>com.XmlEcomBook.catalog.CatalogServ </servlet-class> <init-param> <param-name>workdir</param-name> <param-value>e:\\scripts\\XMLgifts</param-value> </init-param> </servlet>

В листинге 10.2 показана начальная часть файла web.dtd из версии 2.2 API сервлетов. Полная копия файла, содержащая более 400 строк, имеется на прилагаемом к книге компакт-диске.

Листинг 10.2. Часть DTD для web-приложений, в которой показаны элементы первого уровня (web.dtd)

<?xml version="1.0" encoding="ISO-8859-1"?>

<!-- The web-app element is the root of the deployment descriptor for a web application -->

<!ELEMENT web-app (icon?, display-name?, description?, distributable?, context-param*, servlet*, servlet-mapping*, session-config?, mime-mapping*, welcome-file-list?, error-page*, taglib*, resource-ref*, security-constraint*, login-config?, security-role*, env-entry*, ejb-ref*)>



J2EE и Enterprise JavaBean


Система J2EE (Java 2 Enterprise Edition), включающая библиотеки, наборы инструментальных средств и интерфейсов API, — это редакция среды разработки Java, ориентированная на создание многоуровневых крупномасштабных web-приложений. Как показано на рис. 10.1, в подобных web-приложениях действия сер- влетов Java и JSP-страниц сводятся к созданию пользовательского интерфейса, в то время как «конечными» процессами, связанными с бизнес-логикой и доступом к базам данных, управляет Enterprise JavaBean (EJB) — интерфейс API, основанный на Java.

Рис. 10.1. Модель приложения на основе J2EE

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



Определение web-приложения


На этап определения web-приложения API сервлетов не накладывает никаких ограничений. Производители имеют полную свободу в создании собственных систем для определения контекста приложения на сервере. Сервер Tomcat использует файл server.xml для определения элемента ContextManager, содержащего многочисленные элементы Context. Многие из основных серверных функций определены в элементах Context, и они также задействуются для определения web- приложений. Например, ниже мы приводим элемент Context, который мы использовали для данной книги:

<Context path="/XMLbook" docBase="webapps/XMLbook" debug="0"

reload,able="true" > </Context>

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

Также вы можете определить параметры, которые будут доступны всем серв- летам или JSP-страницам данного приложения через объект ServletContext. Ниже приводится пример применения элемента Parameter:

<Context path="/XMLbook" docBase=

"webapps/XMLbook" debug="0"

reloadable="true" >

<Parameter name="workdir" value="e \\scnpts\\XMLgifts" /> </Context>

В Tomcat используется соглашение о том, что корневая папка вашего приложения и ее вложенные папки по умолчанию содержатся во вложенной папке ROOT, как указано в следующем объявлении Context:

<Context path="" docBase="webapps/ROOT" debug="0"

reloadable="true" > </Context>

Папка ROOT содержит файл index.html, который автоматически отображается при вводе URL-адреса, например такого: http://localcost:8080/.



Параметры конфигурации


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

параметры инициализации ServletContext;

конфигурация HttpSession;

определение и отображение сервлетов и JSP-страниц;

отображение типов MIME;

стандартные страницы с приветствием;

страницы ошибок;

параметры безопасности.

Использование параметров каждой из этих категорий подробно описано в официальном руководстве по API сервлетов, поэтому здесь мы не будем рассказывать обо всех параметрах.) Наиболее вероятно, что из всех этих параметров вам потребуются только параметры для определения и отображения сервлетов и JSP-страниц. Например, рассмотрим следующий фрагмент листинга 10.1:

<servlet><servlet-name>catalog</servlet-name>

<servlet-class>com.XmlEcomBook.catalog .CatalogServ</servlet-class>

<init-param>

<param-name>workch r</param-name>

<param-value>e:\\scMpts\\XMLgTfts</param-val.ue>

</init-param> </servlet>

Этот фрагмент отображает класс сервлета в папку имен и создает один параметр инициализации с именем workdir и значением е: \\scripts\\XMLgifts. Здесь используется символ \\, так как одна косая черта \ играет роль управляющего символа для строк Java.



Проблемы масштабирования


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

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

Использование двух или более различных машин в сети для одного и того же сайта часто называется объединением в кластеры, или кластеризацией (clustering). В такой системе имеется один препроцессор, который переадресует запросы одному из серверов в зависимости от некоторого критерия.



Протокол SOAP


Протокол SOAP (Simple Object Access Protocol) — это важный для web-приложений протокол, к которому проявляют огромный интерес программисты, работающие с XML. SOAP определяет стандарты для кодирования различных типов данных и соглашений о представлении вызывающих методов в удаленных объектах. Это «легкий» (light-weight) протокол для обмена сообщениями между объектами в распределенной среде. С использованием XML и передачи простого (plain) текста через стандартный порт HTTP сообщения SOAP могут передаваться через брандмауэры.

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



Развертывание web-приложения


Спецификация 2.2 сервлетов Java достаточно подробно развивает концепцию web-приложений. Web-приложение — это совокупность сервлетов, классов Java, JSP-страниц, HTML-страниц и других ресурсов, которые могут объединяться в единое целое и выполняться в различных контейнерах. Чтобы это стало возможным, web-серверы не должны, как прежде, применять свои собственные схемы создания папок и инициализации; напротив, они должны выполнять определенные соглашения по установке, утвержденные Sun.

Изменения в API для сервлетов ясно говорят о том, что разработчики Java серьезно задумались о вопросах обеспечения безопасности как в отношении ограничения доступа извне, так и в отношении установки барьеров между различными web-приложениями, а также внутри самих приложений. Например, в более ранних версиях API метод getServletNames из класса ServletConext позволил бы одному сервлету определить имена всех сервлетов, выполняющихся в конкретной папке приложения. Метод getServlet затем мог бы получить ссылку на фактический объект сервлета. Также имелся метод getServlets, который возвращал перечень всех экземпляров сервлетов.

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

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



Следующее поколение


Сейчас, когда пишется эта книга, уже выпущена следующая версия API сервлетов, под номером 2.3. Пока она проходит стадию проверки, но к моменту, когда наша книга будет опубликована, эта версия, вероятно, получит официальный статус. Организация Apache работает над версией 4 сервера Tomcat, в котором будут реализованы и API 2.3 для сервлетов, и API 1.2 для JavaServer Pages. Эта новая версия потребует поддержки пакета JDK 1.2 или более поздней его версии.

К добавленным новым средствам относятся фильтры (filters) и слушатели событий (event listeners). Фильтры — это классы Java, которые могут модифицировать содержание запроса перед тем, как он будет передан сервлету, или содержимое ответа, сгенерированного сервлетом. Например, фильтры могут расшифровать запрос или зашифровать ответ. Другой возможностью является применение XSLT-трансформации к данным XML, сгенерированным сервлетом.

Слушатели событий предназначены для того, чтобы дать программисту больше возможностей для контроля над приложением как над единым целым. Например, слушатель событий может получать извещения каждый раз, когда какой-нибудь сервлет меняет атрибут в объекте Servl etContext.

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

В плане обеспечения безопасности также произошли некоторые усовершенствования. Ее реализация в версии API 2.3 сервлетов основана на архитектуре платформы Java 2, поэтому в ней возможно очень тонкое разграничение функций обеспечения безопасности.

В версии API 2.3 сервлетов рекомендуется, но не требуется, поддержка HTTP 1.1. Это означает, что методы сервлетов должны поддерживать запросы PUT, DELETE, OPTIONS и TRACE. Тем не менее предполагается, что эти нововведения в API сервлетов и JSP не нарушат работу приложений, написанных согласно стандартам API 2.2 сервлетов и API 1.1 JSP.



Следующее поколение XML


Версия Tomcat 3.1, которую мы здесь используем, работает с анализатором Sun JAXP 1.0, реализующим только интерфейсы SAX 1.0 и API первого уровня DOM. Версия 1.1 анализатора JAXP будет поддерживать второй уровень DOM и версию 2.0 SAX. Многие изменения всего лишь упрощают настройку характеристик анализатора, но некоторые из них добавляют существенно новые возможности.

В JAXP 1.1 будет включен новый пакет javax.xml .transform для определения интерфейсов, поддерживающих трансформацию данных XML на основе таблиц стилей. В числе других свойств можно выделить создание более обобщенных анализаторов, манипулирование пространствами имен и упрощение настройки многих параметров анализатора.

Если вас интересуют эксперименты с последними новшествами, связанными с поддержкой XML в Java, то в процессе подготовки проекта анализатора Xerces организация Apache продолжает рассматривать новые предложения. Многие из предложенных дополнений находятся в различных стадиях разработки, так что анализатор Xerces является не законченным продуктом, а скорее рабочим проектом. Подробную информацию вы найдете на сайте http:// xml.apache.org/xerces-j/.



Содержимое папки WEB-INF


В папке WEB-INF для web-приложения должен содержаться дескриптор развертывания в виде файла с именем web.xml. Этот файл должен быть согласован с опубликованным компанией Sun определением DTD для дескриптора развертывания web-приложения.

Все файлы классов сервлетов Java, уникальные для данного web-приложения, включая классы, используемые JSP-страницами, должны развертываться во вложенной папке classes, если они получены в виде отдельных файлов классов. В этой папке сохраняется обычная иерархия пакетов, так что, например, файл класса для сервлета QanalysnsServ из пакета com.XmlEcomBook.Chap()? будет содержаться в следующей вложенной папке основной папки приложения:

WEB-INF/classes/com/XmIBook/QanalysisServ.class

Альтернативой хранению отдельных файлов классов во вложенной папке classes является создание библиотеки файлов .jar. Файлы .jar, расположенные в папке WEB-INF/lib, доступны виртуальной машине Java при загрузке классов и других ресурсов для web-приложения. Заметим, что файлы .zip в этой папке будут игнорироваться, поэтому следует использовать файлы с расширением .jar.

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



Сохранность информации о сеансе


Самая большая проблема при разделении нагрузки между несколькими машинами связана с приложениями, которые должны отслеживать состояние пользователя. В приложении «корзина покупателя» мы использовали объект HttpSession, управляемый контейнером сервлетов, чтобы сохранять объект ShoppingCart в промежутках между циклами запрос-ответ. К сожалению, управление объектом HttpSession осуществляется целиком внутри одного объекта ServletContext в одной виртуальной машине Java. Чтобы в условиях кластеризации данные корзины покупателя оставались актуальными в течение всего времени, пока пользователь находится на вашем сайте, в системе должна применяться одна из приведенных ниже технологий.

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

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

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

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



Спецификации в интерфейсе API сервлетов Java


Ниже перечислены ресурсы, согласованная работа которых требуется, чтобы запустить на web-сервере сервлет Java или JSP-приложение:

HTML-страницы;

ресурсы мультимедиа;

файлы классов сервлетов Java;

совместно используемые библиотечные файлы Java;

файлы JSP;

параметры инициализации сервлетов;

параметры безопасности;

доступ к базам данных.

В ранних реализациях API для сервлетов Java не было указано, как организовать все эти ресурсы, поэтому производители создавали собственные системы. По мере того как сложность web-приложений возрастала, пользовательские интерфейсы для установки и управления приложениями становились весьма запутанными. Например, старая версия процессора сервлетов JRun, на одно поколение опередившая версию API 2.2, содержала 259 файлов с расширением .properties, разбросанных по системе папок, с трудом поддававшейся пониманию и напоминающей лабиринт.

Компания Sun вынуждена была разобраться с этой проблемой, работая над пользовательскими и программными интерфейсами J2EE (Java 2 Enterprise Edition — редакция Java 2 для корпоративных программных систем на базе Java) и EJB (Enterprise JavaBean — JavaBean для корпоративных систем). Первые выпуски этих технологий требовали создания дескриптора развертывания (deployment descriptor) для каждого интерфейса EJB. Такой механизм был неудобным и трудным для понимания.

Следующая версия EJB была полностью изменена компанией Sun по сравнению с предыдущей. В версии EJB 1.1 за передачу параметров установки при запуске отвечает специальный документ XML. Тот факт, что компания Sun проявила готовность полностью отказаться от существовавшего интерфейса API и предпочла решение, основанное на XML, еще раз подчеркивает значимость языка XML.



Структура папок


Атрибут doBase определяет базовое расположение файлов приложения относительно установочной папки Tomcat. Предположим, что установочная папка — c:\tomcat, тогда сервер предоставляет по умолчанию папку webapps для установки ваших web-приложений и полный путь к файлам приложения выглядит следующим образом:

с: \tomcat\webapps\XM Lbook

Поступающие на web-сервер запросы, в которых используются URL-адреса типа hhtp://localhost/XMLbook, будут обслуживаться в физической структуре папок относительно указанного пути. Например, пусть пользователь запрашивает файл, указывая следующий URL-адрес:

http://localhost/XMLbook/catalog/index.html

Тогда web-сервер направит этот запрос в физический файл по адресу:

c:\tomcat\webapps\XMLbookcatalog\index.html

Естественно, все запросы на таблицы стилей, файлы изображений и другие ресурсы будут выполняться подобным образом Здесь мы подходим к рассмотрению файла класса (class file), который используется апплетами Java и часто является причиной непонимания. Поскольку файл класса должен быть отправлен web-браузеру, как любой другой ресурс, класс апплета, или JAR-файл, должен храниться с простыми HTML-файлами, а не с файлами классов, используемыми сервлетами.

Чтобы отделить файлы классов и библиотеки классов, используемые приложением, от простых HTML-страниц, файлов изображений, файлов классов апп- летов и других ресурсов, в версии API 2.2 для сервлетов требуется, чтобы в папке приложения имелась вложенная папка WEB-IN F. Web-серверу запрещается посылать любые ресурсы из этой папки в ответ на запрос пользователя.

Если приложение было разработано как WAR-файл (более подробно мы поговорим об этих файлах позже), то должна присутствовать папка с именем МЕТА- INF. Смысл здесь в том, что данная папка будет содержать дополнительную информацию о приложении. Как и в отношении папки WEB-INF, web-сервер не имеет права отправлять пользователю любые содержащиеся в этой папке ресурсы.