Класс 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() ; }
}