001: /*---------------------------------------------------------------------------*\
002: $Id: XMLDataPersister.java 6962 2007-08-12 04:54:23Z bmc $
003: \*---------------------------------------------------------------------------*/
004:
005: package org.clapper.curn;
006:
007: import java.io.File;
008: import java.io.IOException;
009: import java.io.Writer;
010: import java.net.MalformedURLException;
011: import java.net.URL;
012: import java.util.ArrayList;
013: import java.util.Collection;
014: import java.util.Date;
015: import java.util.HashMap;
016: import java.util.Iterator;
017: import java.util.List;
018: import java.util.Map;
019:
020: import org.clapper.util.config.ConfigurationException;
021: import org.clapper.util.io.IOExceptionExt;
022: import org.clapper.util.logging.Logger;
023: import org.clapper.util.text.TextUtil;
024: import org.jdom.Document;
025: import org.jdom.Element;
026: import org.jdom.input.SAXBuilder;
027: import org.jdom.output.Format;
028: import org.jdom.output.XMLOutputter;
029:
030: /**
031: * @version <tt>$Revision: 6962 $</tt>
032: */
033: public class XMLDataPersister extends DataPersister {
034: /*----------------------------------------------------------------------*\
035: Private Constants
036: \*----------------------------------------------------------------------*/
037:
038: private static final int DEF_TOTAL_CACHE_BACKUPS = 0;
039:
040: private static final String VAR_CACHE_FILE = "CacheFile";
041: private static final String VAR_TOTAL_CACHE_BACKUPS = "TotalCacheBackups";
042:
043: /**
044: * Constants for the old XML format
045: */
046: private static final String OLD_XML_ROOT_ELEMENT = "curn_cache";
047: private static final String OLD_XML_ENTRY_ELEMENT = "cache_entry";
048: private static final String OLD_XML_ENTRY_TIMESTAMP_ATTR = "timestamp";
049: private static final String OLD_XML_ENTRY_CHANNEL_URL_ATTR = "channel_URL";
050: private static final String OLD_XML_ENTRY_ENTRY_URL_ATTR = "entry_URL";
051: private static final String OLD_XML_ENTRY_ENTRY_ID_ATTR = "entry_ID";
052: private static final String OLD_XML_ENTRY_PUB_DATE_ATTR = "pub_date";
053:
054: /**
055: * Constants for the new XML format.
056: */
057: private static final String XML_ROOT_ELEMENT = "curn-data";
058: private static final String XML_FEED_ELEMENT = "feed";
059: private static final String XML_ITEM_ELEMENT = "item";
060: private static final String XML_ITEM_METADATA_ELEMENT = "item-metadata";
061: private static final String XML_FEED_METADATA_ELEMENT = "feed-metadata";
062: private static final String XML_EXTRA_METADATA_ELEMENT = "extra-metadata";
063: private static final String XML_METADATA_NAMESPACE_ATTR = "namespace";
064: private static final String XML_METADATUM_ELEMENT = "metadatum";
065: private static final String XML_METADATUM_NAME_ATTR = "name";
066: private static final String XML_METADATUM_VALUE_ATTR = "value";
067: private static final String XML_TIMESTAMP_ATTR = "timestamp";
068: private static final String XML_URL_ATTR = "url";
069: private static final String XML_ID_ATTR = "id";
070: private static final String XML_PUB_DATE_ATTR = "pub-date";
071:
072: /*----------------------------------------------------------------------*\
073: Private Instance Data
074: \*----------------------------------------------------------------------*/
075:
076: private int totalCacheBackups = DEF_TOTAL_CACHE_BACKUPS;
077: private File metadataFile = null;
078:
079: /**
080: * Root XML element, used while saving.
081: */
082: private Element rootElementForSaving = null;
083:
084: /**
085: * For logging
086: */
087: private static final Logger log = new Logger(XMLDataPersister.class);
088:
089: /*----------------------------------------------------------------------*\
090: Constructor
091: \*----------------------------------------------------------------------*/
092:
093: /**
094: * Creates a new instance of XMLDataPersister
095: */
096: public XMLDataPersister() {
097: }
098:
099: /*----------------------------------------------------------------------*\
100: Public Methods
101: \*----------------------------------------------------------------------*/
102:
103: /**
104: * Called when the <tt>DataPersister</tt> is first instantiated. Useful
105: * for retrieving configuration values, etc.
106: *
107: * @param curnConfig the configuration
108: * @throws CurnException on error
109: */
110: public void init(CurnConfig curnConfig) throws CurnException {
111: try {
112: String cacheFileName = curnConfig.getOptionalStringValue(
113: CurnConfig.MAIN_SECTION, VAR_CACHE_FILE, null);
114: if (cacheFileName != null) {
115: metadataFile = CurnUtil
116: .mapConfiguredPathName(cacheFileName);
117: if (metadataFile.isDirectory()) {
118: throw new CurnException(
119: Constants.BUNDLE_NAME,
120: "XMLDataPersister.cacheIsDir",
121: "Configured XML cache file \"{0}\" is a directory.",
122: new Object[] { metadataFile.getPath() });
123: }
124: }
125:
126: totalCacheBackups = curnConfig.getOptionalCardinalValue(
127: CurnConfig.MAIN_SECTION, VAR_TOTAL_CACHE_BACKUPS,
128: DEF_TOTAL_CACHE_BACKUPS);
129: }
130:
131: catch (ConfigurationException ex) {
132: throw new CurnException(ex);
133: }
134: }
135:
136: /*----------------------------------------------------------------------*\
137: Protected Methods
138: \*----------------------------------------------------------------------*/
139:
140: /**
141: * Determine whether the data persister subclass is enabled or not (i.e.,
142: * whether or not metadata is to be loaded and saved). The configuration
143: * usually determines whether or not the data persister is enabled.
144: *
145: * @return <tt>true</tt> if enabled, <tt>false</tt> if disabled.
146: */
147: protected boolean isEnabled() {
148: return metadataFile != null;
149: }
150:
151: /**
152: * Called at the beginning of the actual save operation to initialize
153: * the save, etc.
154: *
155: * @throws CurnException on error
156: */
157: protected void startSaveOperation() throws CurnException {
158: assert (isEnabled());
159:
160: log.debug("Saving feed metadata to \"" + metadataFile.getPath()
161: + "\". Total backups=" + totalCacheBackups);
162:
163: // Create the DOM's root element.
164:
165: rootElementForSaving = new Element(XML_ROOT_ELEMENT);
166: rootElementForSaving.setAttribute(XML_TIMESTAMP_ATTR, String
167: .valueOf(System.currentTimeMillis()));
168: }
169:
170: /**
171: * Called at the end of the actual save operation to flush files, clean
172: * up, etc.
173: *
174: * @throws CurnException on error
175: */
176: protected void endSaveOperation() throws CurnException {
177: try {
178: Document document = new Document(rootElementForSaving);
179: Format outputFormat = Format.getPrettyFormat();
180: outputFormat.setLineSeparator(System
181: .getProperty("line.separator"));
182: XMLOutputter xmlOut = new XMLOutputter(outputFormat);
183:
184: // Open the cache file. For the cache file, the index
185: // marker goes at the end of the file (since the extension
186: // doesn't matter as much). This allows the file names to
187: // sort better in a directory listing.
188:
189: Writer cacheOut = CurnUtil.openOutputFile(metadataFile,
190: null, CurnUtil.IndexMarker.AFTER_EXTENSION,
191: totalCacheBackups);
192:
193: //xmlOut.output(document, new XMLWriter(cacheOut));
194: xmlOut.output(document, cacheOut);
195: }
196:
197: catch (IOException ex) {
198: throw new CurnException("Failed to write XML cache file \""
199: + metadataFile.getPath() + "\"", ex);
200: }
201:
202: catch (IOExceptionExt ex) {
203: throw new CurnException("Failed to write XML cache file \""
204: + metadataFile.getPath() + "\"", ex);
205: }
206: }
207:
208: /**
209: * Save the data for one feed, including the items.
210: *
211: * @param feedData the feed data to be saved
212: *
213: * @throws CurnException on error
214: */
215: protected void saveFeedData(PersistentFeedData feedData)
216: throws CurnException {
217: FeedCacheEntry feedCacheData = feedData.getFeedCacheEntry();
218: URL channelURL = feedCacheData.getChannelURL();
219:
220: Element channelElement = new Element(XML_FEED_ELEMENT);
221: channelElement
222: .setAttribute(XML_URL_ATTR, channelURL.toString());
223: channelElement.setAttribute(XML_TIMESTAMP_ATTR, String
224: .valueOf(feedCacheData.getTimestamp()));
225: channelElement.setAttribute(XML_ID_ATTR, feedCacheData
226: .getUniqueID());
227: rootElementForSaving.addContent(channelElement);
228:
229: // Now the feed metadata
230:
231: fillInMetadata(feedData.getFeedMetadata(),
232: XML_FEED_METADATA_ELEMENT, channelElement);
233:
234: // Okay, the feed element has been built. Time to add the items to it.
235:
236: for (PersistentFeedItemData itemData : feedData
237: .getPersistentFeedItems()) {
238: Element itemElement = new Element(XML_ITEM_ELEMENT);
239: channelElement.addContent(itemElement);
240:
241: FeedCacheEntry itemCacheData = itemData.getFeedCacheEntry();
242: itemElement.setAttribute(XML_TIMESTAMP_ATTR, String
243: .valueOf(itemCacheData.getTimestamp()));
244: itemElement.setAttribute(XML_ID_ATTR, itemCacheData
245: .getUniqueID());
246: itemElement.setAttribute(XML_URL_ATTR, itemCacheData
247: .getEntryURL().toString());
248:
249: // Only write the publication date if it's present.
250:
251: Date pubDate = itemCacheData.getPublicationDate();
252: if (pubDate != null) {
253: itemElement.setAttribute(XML_PUB_DATE_ATTR, String
254: .valueOf(pubDate.getTime()));
255: }
256:
257: // Fill in the metadata for the item.
258:
259: fillInMetadata(itemData.getItemMetadata(),
260: XML_ITEM_METADATA_ELEMENT, itemElement);
261: }
262: }
263:
264: /**
265: * Save any extra metadata (i.e., metadata that isn't attached to a
266: * specific feed or a specific item).
267: *
268: * @param metadata the collection of metadata items
269: *
270: * @throws CurnException on error
271: */
272: protected void saveExtraMetadata(
273: Collection<PersistentMetadataGroup> metadata)
274: throws CurnException {
275: fillInMetadata(metadata, XML_EXTRA_METADATA_ELEMENT,
276: rootElementForSaving);
277: }
278:
279: /**
280: * Called at the beginning of the load operation to initialize
281: * the load.
282: *
283: * @throws CurnException on error
284: */
285: protected void startLoadOperation() throws CurnException {
286: assert (isEnabled());
287: log.debug("Starting load of XML curn data.");
288: }
289:
290: /**
291: * Called at the end of the load operation to close files, clean
292: * up, etc.
293: *
294: * @throws CurnException on error
295: */
296: protected void endLoadOperation() throws CurnException {
297: log.debug("Load of XML curn data complete.");
298: }
299:
300: /**
301: * The actual load method; only called if the object is enabled.
302: *
303: * @param loadedDataHandler object to receive data as it's loaded
304: *
305: * @throws CurnException on error
306: */
307: protected void doLoad(LoadedDataHandler loadedDataHandler)
308: throws CurnException {
309: String filePath = metadataFile.getPath();
310: if (metadataFile.exists()) {
311: log
312: .debug("Reading feed metadata from \"" + filePath
313: + "\"");
314:
315: // First, parse the XML file into a DOM.
316:
317: SAXBuilder builder = new SAXBuilder();
318: Document document;
319:
320: log
321: .info("Attempting to parse \"" + filePath
322: + "\" as XML.");
323: try {
324: document = builder.build(metadataFile);
325: }
326:
327: catch (Throwable ex) {
328: log.error(ex);
329: throw new CurnException(ex);
330: }
331:
332: log.debug("XML parse succeeded.");
333:
334: // Get the top-level element and verify that it's the one
335: // we want.
336:
337: Element root = document.getRootElement();
338: String rootTagName = root.getName();
339:
340: if (rootTagName.equals(OLD_XML_ROOT_ELEMENT)) {
341: log.debug("Reading old-style <" + OLD_XML_ROOT_ELEMENT
342: + "> cache file.");
343: readOldXMLCache(document, filePath, loadedDataHandler);
344: }
345:
346: else if (rootTagName.equals(XML_ROOT_ELEMENT)) {
347: log.debug("Reading new-style <" + XML_ROOT_ELEMENT
348: + "> metadata file.");
349: readNewXMLMetaData(document, loadedDataHandler);
350: }
351:
352: else {
353: throw new CurnException(
354: Constants.BUNDLE_NAME,
355: "XMLDataPersister.nonCacheXML",
356: "File \"{0}\" is not a curn XML metadata file. The root "
357: + "XML element is <{1}>, not the expected <{2}> or <{3}>",
358: new Object[] { filePath, rootTagName,
359: OLD_XML_ROOT_ELEMENT, XML_ROOT_ELEMENT });
360: }
361: }
362: }
363:
364: /*----------------------------------------------------------------------*\
365: Private Methods
366: \*----------------------------------------------------------------------*/
367:
368: private void fillInMetadata(
369: Collection<PersistentMetadataGroup> metadata,
370: String elementName, Element parentElement) {
371: for (PersistentMetadataGroup metadataGroup : metadata) {
372: Element metadataElement = new Element(elementName);
373: metadataElement.setAttribute(XML_METADATA_NAMESPACE_ATTR,
374: metadataGroup.getNamespace());
375: parentElement.addContent(metadataElement);
376:
377: Map<String, String> nameValuePairs = metadataGroup
378: .getMetadata();
379: for (Map.Entry<String, String> nameValuePair : nameValuePairs
380: .entrySet()) {
381: Element metadatumElement = new Element(
382: XML_METADATUM_ELEMENT);
383: metadatumElement.setAttribute(XML_METADATUM_NAME_ATTR,
384: nameValuePair.getKey());
385: metadatumElement.setAttribute(XML_METADATUM_VALUE_ATTR,
386: nameValuePair.getValue());
387: metadataElement.addContent(metadatumElement);
388: }
389: }
390: }
391:
392: /**
393: * Attempt to parse an old-style XML cache. This method will go away
394: * soon.
395: *
396: * @param document the parsed XML document
397: * @param filePath the path to the file, for errors
398: * @param loadedDataHandler the callback to invoke with loaded data
399: *
400: * @throws CurnException on error
401: */
402: private void readOldXMLCache(final Document document,
403: final String filePath,
404: final LoadedDataHandler loadedDataHandler)
405: throws CurnException {
406: // Get the top-level element and verify that it's the one
407: // we want.
408:
409: Element root = document.getRootElement();
410: String rootTagName = root.getName();
411: assert (rootTagName.equals(OLD_XML_ROOT_ELEMENT));
412:
413: // Okay, it's a curn cache. Start traversing the child nodes,
414: // parsing each cache entry.
415:
416: Map<URL, PersistentFeedData> loadedData = new HashMap<URL, PersistentFeedData>();
417:
418: List<?> childNodes = root.getChildren();
419: for (Iterator<?> it = childNodes.iterator(); it.hasNext();) {
420: Element childNode = (Element) it.next();
421:
422: // Skip non-element nodes (like text).
423:
424: String nodeName = childNode.getName();
425: if (!nodeName.equals(OLD_XML_ENTRY_ELEMENT)) {
426: log.warn("Skipping unexpected XML element <" + nodeName
427: + "> in curn XML cache file \"" + filePath
428: + "\".");
429: continue;
430: }
431:
432: try {
433: FeedCacheEntry entry = parseOldXMLCacheEntry(childNode);
434: URL feedURL = entry.getChannelURL();
435: PersistentFeedData feedData = loadedData.get(feedURL);
436: log.debug("readOldXMLCache: read entry "
437: + entry.getEntryURL());
438: if (feedData == null) {
439: feedData = new PersistentFeedData();
440: loadedData.put(feedURL, feedData);
441: }
442:
443: if (entry.isChannelEntry()) {
444: feedData.setFeedCacheEntry(entry);
445: }
446:
447: else {
448: feedData
449: .addPersistentFeedItem(new PersistentFeedItemData(
450: entry));
451: }
452: }
453:
454: catch (CurnException ex) {
455: // Bad entry. Log the error, but move on.
456:
457: log.error("Error parsing feed cache entry", ex);
458: }
459: }
460:
461: for (PersistentFeedData feedData : loadedData.values())
462: loadedDataHandler.feedLoaded(feedData);
463: }
464:
465: /**
466: * Parse an old-style XML feed cache entry. This method will go away
467: * soon.
468: *
469: * @param element the XML element for the feed cache entry
470: *
471: * @return the FeedCacheEntry
472: *
473: * @throws CurnException on error
474: */
475: private FeedCacheEntry parseOldXMLCacheEntry(final Element element)
476: throws CurnException {
477: FeedCacheEntry result = null;
478:
479: // Parse out the attributes.
480:
481: String entryID = getRequiredXMLAttribute(element,
482: OLD_XML_ENTRY_ENTRY_ID_ATTR);
483: String sChannelURL = getRequiredXMLAttribute(element,
484: OLD_XML_ENTRY_CHANNEL_URL_ATTR);
485: String sEntryURL = getRequiredXMLAttribute(element,
486: OLD_XML_ENTRY_ENTRY_URL_ATTR);
487: String sTimestamp = getRequiredXMLAttribute(element,
488: OLD_XML_ENTRY_TIMESTAMP_ATTR);
489: String sPubDate = getOptionalXMLAttribute(element,
490: OLD_XML_ENTRY_PUB_DATE_ATTR, null);
491:
492: if ((entryID != null) && (sChannelURL != null)
493: && (sEntryURL != null) && (sTimestamp != null)) {
494: // Parse the timestamp.
495:
496: long timestamp = 0;
497: try {
498: timestamp = Long.parseLong(sTimestamp);
499: }
500:
501: catch (NumberFormatException ex) {
502: throw new CurnException("Bad timestamp value of \""
503: + sTimestamp + "\" for <"
504: + OLD_XML_ENTRY_ELEMENT + "> with unique ID \""
505: + entryID + "\". Skipping entry.");
506: }
507:
508: // Parse the publication date, if any
509:
510: Date publicationDate = null;
511: if (sPubDate != null) {
512: try {
513: publicationDate = new Date(Long.parseLong(sPubDate));
514: }
515:
516: catch (NumberFormatException ex) {
517: log.error("Bad publication date value of \""
518: + sPubDate + "\" for <"
519: + OLD_XML_ENTRY_ELEMENT
520: + "> with unique ID \"" + entryID
521: + "\". Ignoring publication date.");
522: }
523: }
524:
525: // Parse the URLs.
526:
527: URL channelURL = null;
528: try {
529: channelURL = new URL(sChannelURL);
530: }
531:
532: catch (MalformedURLException ex) {
533: throw new CurnException("Bad channel URL \""
534: + sChannelURL + "\" for <"
535: + OLD_XML_ENTRY_ELEMENT + "> with unique ID \""
536: + entryID + "\". Skipping entry.");
537: }
538:
539: URL entryURL = null;
540: try {
541: entryURL = new URL(sEntryURL);
542: }
543:
544: catch (MalformedURLException ex) {
545: throw new CurnException("Bad item URL \"" + sChannelURL
546: + "\" for <" + OLD_XML_ENTRY_ELEMENT
547: + "> with unique ID \"" + entryID
548: + "\". Skipping entry.");
549: }
550:
551: result = new FeedCacheEntry(entryID, channelURL, entryURL,
552: publicationDate, timestamp);
553: }
554:
555: return result;
556: }
557:
558: /**
559: * Attempt to parse a new-style XML metadata file.
560: *
561: * @param document the parsed XML file
562: * @param loadedDataHandler the callback to invoke with loaded data
563: *
564: * @throws CurnException on error
565: */
566: private void readNewXMLMetaData(final Document document,
567: final LoadedDataHandler loadedDataHandler)
568: throws CurnException {
569: // Get the top-level element and verify that it's the one
570: // we want.
571:
572: Element root = document.getRootElement();
573: String rootTagName = root.getName();
574: assert (rootTagName.equals(XML_ROOT_ELEMENT));
575:
576: // Get the list of channels.
577:
578: List<?> channels = root.getChildren(XML_FEED_ELEMENT);
579: for (Iterator<?> itChannel = channels.iterator(); itChannel
580: .hasNext();) {
581: // Parse the channel element itself.
582:
583: Element channelElement = (Element) itChannel.next();
584: FeedCacheEntry entry = parseXMLFeedElement(channelElement);
585: PersistentFeedData feedData = new PersistentFeedData(entry);
586: URL channelURL = entry.getChannelURL();
587:
588: // Parse any metadata in the channel.
589:
590: List<?> feedMetadata = channelElement
591: .getChildren(XML_FEED_METADATA_ELEMENT);
592: feedData.addFeedMetadata(parseMetadata(feedMetadata));
593:
594: // Get the list of items and process each one.
595:
596: List<?> items = channelElement
597: .getChildren(XML_ITEM_ELEMENT);
598: for (Iterator<?> itItem = items.iterator(); itItem
599: .hasNext();) {
600: // Parse the item element itself.
601:
602: Element itemElement = (Element) itItem.next();
603: entry = parseXMLItemElement(itemElement, channelURL);
604: PersistentFeedItemData itemData = new PersistentFeedItemData(
605: entry);
606: feedData.addPersistentFeedItem(itemData);
607:
608: // Get and process the item metadata
609:
610: List<?> itemMetadata = itemElement
611: .getChildren(XML_ITEM_METADATA_ELEMENT);
612: itemData.addItemMetadata(parseMetadata(itemMetadata));
613: }
614:
615: loadedDataHandler.feedLoaded(feedData);
616: }
617:
618: // Finally, parse any extra metadata.
619:
620: Collection<PersistentMetadataGroup> extraMetadata = parseMetadata(root
621: .getChildren(XML_EXTRA_METADATA_ELEMENT));
622: for (PersistentMetadataGroup metadataGroup : extraMetadata)
623: loadedDataHandler.extraMetadataLoaded(metadataGroup);
624: }
625:
626: private Collection<PersistentMetadataGroup> parseMetadata(
627: List<?> metadataElements) {
628: Collection<PersistentMetadataGroup> result = new ArrayList<PersistentMetadataGroup>();
629:
630: for (Iterator<?> it = metadataElements.iterator(); it.hasNext();) {
631: Element mdElement = (Element) it.next();
632: String namespace = getRequiredXMLAttribute(mdElement,
633: XML_METADATA_NAMESPACE_ATTR);
634: List<?> nameValuePairs = mdElement
635: .getChildren(XML_METADATUM_ELEMENT);
636: PersistentMetadataGroup metadataGroup = new PersistentMetadataGroup(
637: namespace);
638:
639: for (Iterator<?> itNV = nameValuePairs.iterator(); itNV
640: .hasNext();) {
641: Element nvElement = (Element) itNV.next();
642: String name = getRequiredXMLAttribute(nvElement,
643: XML_METADATUM_NAME_ATTR);
644: String value = getRequiredXMLAttribute(nvElement,
645: XML_METADATUM_VALUE_ATTR);
646: metadataGroup.addMetadataItem(name, value);
647: }
648:
649: result.add(metadataGroup);
650: }
651:
652: return result;
653: }
654:
655: /**
656: * Parse an XML feed metadata channel element. This method only parses the
657: * attributes of the channel element; it does not handle any child
658: * elements.
659: *
660: * @param channelElement the XML element for the channel
661: *
662: * @return the FeedCacheEntry object for the feed
663: *
664: * @throws CurnException on error
665: */
666: private FeedCacheEntry parseXMLFeedElement(
667: final Element channelElement) throws CurnException {
668: // Parse the channel and create an entry for it.
669:
670: String sChannelURL = getRequiredXMLAttribute(channelElement,
671: XML_URL_ATTR);
672: String id = getRequiredXMLAttribute(channelElement, XML_ID_ATTR);
673: String sTimestamp = getRequiredXMLAttribute(channelElement,
674: XML_TIMESTAMP_ATTR);
675:
676: long timestamp = 0;
677: try {
678: timestamp = Long.parseLong(sTimestamp);
679: }
680:
681: catch (NumberFormatException ex) {
682: throw new CurnException("Bad timestamp value of \""
683: + sTimestamp + "\" for <" + XML_FEED_ELEMENT
684: + "> with unique ID \"" + id
685: + "\". Skipping entry.");
686: }
687:
688: URL channelURL;
689: try {
690: channelURL = new URL(sChannelURL);
691: }
692:
693: catch (MalformedURLException ex) {
694: throw new CurnException("Bad channel URL \"" + sChannelURL
695: + "\" for <" + XML_FEED_ELEMENT
696: + "> with unique ID \"" + id + "\"");
697: }
698:
699: FeedCacheEntry entry = new FeedCacheEntry(id, channelURL,
700: channelURL, null, timestamp);
701:
702: // Now the feed metadata.
703:
704: return entry;
705: }
706:
707: /**
708: * Parse an XML feed metadata item entry.
709: *
710: * @param itemElement the XML element for the item entry
711: * @param channelURL the URL of the parent channel
712: *
713: * @return the FeedCacheEntry
714: *
715: * @throws CurnException on error
716: */
717: private FeedCacheEntry parseXMLItemElement(
718: final Element itemElement, final URL channelURL)
719: throws CurnException {
720: FeedCacheEntry result = null;
721:
722: // Parse out the attributes.
723:
724: String id = getRequiredXMLAttribute(itemElement, XML_ID_ATTR);
725: String sItemURL = getRequiredXMLAttribute(itemElement,
726: XML_URL_ATTR);
727: String sTimestamp = getRequiredXMLAttribute(itemElement,
728: XML_TIMESTAMP_ATTR);
729: String sPubDate = getOptionalXMLAttribute(itemElement,
730: XML_PUB_DATE_ATTR, null);
731:
732: if ((id != null) && (sItemURL != null) && (sTimestamp != null)) {
733: // Parse the timestamp.
734:
735: long timestamp = 0;
736: try {
737: timestamp = Long.parseLong(sTimestamp);
738: }
739:
740: catch (NumberFormatException ex) {
741: throw new CurnException("Bad timestamp value of \""
742: + sTimestamp + "\" for <" + XML_ITEM_ELEMENT
743: + "> with unique ID \"" + id
744: + "\". Skipping entry.");
745: }
746:
747: // Parse the publication date, if any
748:
749: Date publicationDate = null;
750: if (sPubDate != null) {
751: try {
752: long pubTimestamp = Long.parseLong(sPubDate);
753: if (pubTimestamp > 0)
754: publicationDate = new Date(pubTimestamp);
755: }
756:
757: catch (NumberFormatException ex) {
758: log.error("Bad publication date value of \""
759: + sPubDate + "\" for <" + XML_ITEM_ELEMENT
760: + "> with unique ID \"" + id
761: + "\". Ignoring publication date.");
762: }
763: }
764:
765: // Parse the URL.
766:
767: URL itemURL = null;
768: try {
769: itemURL = new URL(sItemURL);
770: }
771:
772: catch (MalformedURLException ex) {
773: throw new CurnException("Bad item URL \"" + sItemURL
774: + "\" for <" + XML_ITEM_ELEMENT
775: + "> with unique ID \"" + id
776: + "\". Skipping entry.");
777: }
778:
779: result = new FeedCacheEntry(id, channelURL, itemURL,
780: publicationDate, timestamp);
781: }
782:
783: return result;
784: }
785:
786: /**
787: * Retrieve an optional XML attribute value from a list of attributes.
788: * If the attribute is missing or empty, the default is returned.
789: *
790: * @param element the XML element
791: * @param defaultValue the default value
792: * @param name the attribute name
793: *
794: * @return the attribute's value, or null if the attribute wasn't found
795: */
796: private String getOptionalXMLAttribute(final Element element,
797: final String name, final String defaultValue) {
798: String value = element.getAttributeValue(name);
799: if ((value != null) && TextUtil.stringIsEmpty(value))
800: value = null;
801:
802: return (value == null) ? defaultValue : value;
803: }
804:
805: /**
806: * Retrieve an XML attribute value from a list of attributes. If the
807: * attribute is missing, the error is logged (but an exception is not
808: * thrown).
809: *
810: * @param element the element
811: * @param name the attribute name
812: *
813: * @return the attribute's value, or null if the attribute wasn't found
814: */
815: private String getRequiredXMLAttribute(final Element element,
816: final String name) {
817: String value = getOptionalXMLAttribute(element, name, null);
818:
819: if (value == null) {
820: log.error("<" + element.getName()
821: + "> is missing required " + "\"" + name
822: + "\" XML attribute.");
823: }
824:
825: return value;
826: }
827: }
|