001: /*---------------------------------------------------------------------------*\
002: $Id: RSSItem.java 7041 2007-09-09 01:04:47Z bmc $
003: ---------------------------------------------------------------------------
004: This software is released under a BSD-style license:
005:
006: Copyright (c) 2004-2007 Brian M. Clapper. All rights reserved.
007:
008: Redistribution and use in source and binary forms, with or without
009: modification, are permitted provided that the following conditions are
010: met:
011:
012: 1. Redistributions of source code must retain the above copyright notice,
013: this list of conditions and the following disclaimer.
014:
015: 2. The end-user documentation included with the redistribution, if any,
016: must include the following acknowlegement:
017:
018: "This product includes software developed by Brian M. Clapper
019: (bmc@clapper.org, http://www.clapper.org/bmc/). That software is
020: copyright (c) 2004-2007 Brian M. Clapper."
021:
022: Alternately, this acknowlegement may appear in the software itself,
023: if wherever such third-party acknowlegements normally appear.
024:
025: 3. Neither the names "clapper.org", "curn", nor any of the names of the
026: project contributors may be used to endorse or promote products
027: derived from this software without prior written permission. For
028: written permission, please contact bmc@clapper.org.
029:
030: 4. Products derived from this software may not be called "curn", nor may
031: "clapper.org" appear in their names without prior written permission
032: of Brian M. Clapper.
033:
034: THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
035: WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
036: MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
037: NO EVENT SHALL BRIAN M. CLAPPER BE LIABLE FOR ANY DIRECT, INDIRECT,
038: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
039: NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
040: DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
041: THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
042: (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
043: THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
044: \*---------------------------------------------------------------------------*/
045:
046: package org.clapper.curn.parser;
047:
048: import org.clapper.util.text.TextUtil;
049:
050: import java.util.Collection;
051: import java.util.Date;
052: import java.util.HashMap;
053: import java.util.Map;
054:
055: /**
056: * This abstract class defines a simplified view of an RSS item, providing
057: * only the methods necessary for <i>curn</i> to work. <i>curn</i> uses the
058: * {@link RSSParserFactory} class to get a specific implementation of
059: * <tt>RSSParser</tt>, which returns <tt>RSSChannel</tt>-conforming objects
060: * that, in turn, return item objects that subclass <tt>RSSItem</tt>. This
061: * strategy isolates the bulk of the code from the underlying RSS parser,
062: * making it easier to substitute different parsers as more of them become
063: * available. <tt>RSSItem</tt>. This strategy isolates the bulk of the code
064: * from the underlying RSS parser, making it easier to substitute different
065: * parsers as more of them become available.
066: *
067: * @see RSSParserFactory
068: * @see RSSParser
069: * @see RSSChannel
070: *
071: * @version <tt>$Revision: 7041 $</tt>
072: */
073: public abstract class RSSItem extends RSSElement implements Cloneable,
074: Comparable {
075: /*----------------------------------------------------------------------*\
076: Constants
077: \*----------------------------------------------------------------------*/
078:
079: /**
080: * Constant defining the pseudo-MIME type to use for default content.
081: */
082: public static final String DEFAULT_CONTENT_TYPE = "*";
083:
084: /**
085: * Unlimited summary size
086: */
087: public static final int NO_SUMMARY_LIMIT = 0;
088:
089: /*----------------------------------------------------------------------*\
090: Private Instance Data
091: \*----------------------------------------------------------------------*/
092:
093: private HashMap<String, String> contentMap = null;
094:
095: /*----------------------------------------------------------------------*\
096: Constructor
097: \*----------------------------------------------------------------------*/
098:
099: /**
100: * Default constructor
101: */
102: protected RSSItem() {
103: super ();
104: }
105:
106: /*----------------------------------------------------------------------*\
107: Public Methods
108: \*----------------------------------------------------------------------*/
109:
110: /**
111: * Clone this channel. This method simply calls the type-safe
112: * {@link #makeCopy} method. The clone is a deep-clone (i.e., the items
113: * are cloned, too).
114: *
115: * @return the cloned <tt>RSSChannel</tt>
116: *
117: * @throws CloneNotSupportedException doesn't, actually, but the
118: * <tt>Cloneable</tt> interface
119: * requires that this exception
120: * be declared
121: *
122: * @see #makeCopy
123: */
124: @Override
125: public Object clone() throws CloneNotSupportedException {
126: return makeCopy(getParentChannel());
127: }
128:
129: /**
130: * Make a deep copy of this <tt>RSSItem</tt> object.
131: *
132: * @param parentChannel the parent channel to assign to the new instance
133: *
134: * @return the copy
135: */
136: public RSSItem makeCopy(RSSChannel parentChannel) {
137: RSSItem copy = newInstance(parentChannel);
138:
139: initContentMap();
140: copy.contentMap = new HashMap<String, String>();
141: for (String key : this .contentMap.keySet())
142: copy.contentMap.put(key, this .contentMap.get(key));
143:
144: copyPrivateFields(copy);
145: copy.setTitle(this .getTitle());
146: copy.setSummary(this .getSummary());
147: copy.setLinks(this .getLinks());
148: copy.setCategories(this .getCategories());
149: copy.setPublicationDate(this .getPublicationDate());
150: copy.setID(this .getID());
151:
152: Collection<String> authors = this .getAuthors();
153: if (authors != null) {
154: for (String author : authors) {
155: if (author != null)
156: copy.addAuthor(author);
157: }
158: }
159:
160: return copy;
161: }
162:
163: /**
164: * Get the item's content, if available. Some feed types (e.g., Atom)
165: * support multiple content sections, each with its own MIME type; the
166: * <tt>mimeType</tt> parameter specifies the caller's desired MIME
167: * type.
168: *
169: * @param mimeType the desired MIME type
170: *
171: * @return the content (or the default content), or null if no content
172: * of the desired MIME type is available
173: *
174: * @see #clearContent
175: * @see #getFirstContentOfType
176: * @see #setContent
177: */
178: public String getContent(String mimeType) {
179: String result = null;
180:
181: initContentMap();
182: result = contentMap.get(mimeType);
183: if (result == null)
184: result = contentMap.get(DEFAULT_CONTENT_TYPE);
185:
186: return result;
187: }
188:
189: /**
190: * Get the first content item that matches one of a list of MIME types.
191: *
192: * @param mimeTypes an array of MIME types to match, in order
193: *
194: * @return the first matching content string, or null if none was found.
195: * Returns the default content (if set), if there's no exact
196: * match.
197: *
198: * @see #getContent
199: * @see #clearContent
200: * @see #setContent
201: */
202: public final String getFirstContentOfType(String... mimeTypes) {
203: String result = null;
204:
205: initContentMap();
206: for (int i = 0; i < mimeTypes.length; i++) {
207: result = contentMap.get(mimeTypes[i]);
208: if (!TextUtil.stringIsEmpty(result))
209: break;
210: }
211:
212: if (result == null)
213: result = contentMap.get(DEFAULT_CONTENT_TYPE);
214:
215: return result;
216: }
217:
218: /**
219: * Set the content for a specific MIME type. If the
220: * <tt>isDefault</tt> flag is <tt>true</tt>, then this content
221: * is served up as the default whenever content for a specific MIME type
222: * is requested but isn't available.
223: *
224: * @param content the content string
225: * @param mimeType the MIME type to associate with the content
226: *
227: * @see #getContent
228: * @see #getFirstContentOfType
229: * @see #clearContent
230: */
231: public void setContent(String content, String mimeType) {
232: initContentMap();
233: contentMap.put(mimeType, content);
234: }
235:
236: /**
237: * Clear the stored content for all MIME types, without clearing any
238: * other fields. (In particular, the summary is not cleared.)
239: *
240: * @see #getContent
241: * @see #getFirstContentOfType
242: * @see #setContent
243: */
244: public void clearContent() {
245: if (contentMap != null)
246: contentMap.clear();
247: }
248:
249: /**
250: * Compare two items for order. The channels are ordered first by
251: * publication date (if any), then by title, then by unique ID,
252: * then by hash code (if all else is equal).
253: *
254: * @param other the other object
255: *
256: * @return negative number: this item is less than <tt>other</tt>;<br>
257: * 0: this item is equivalent to <tt>other</tt><br>
258: * positive unmber: this item is greater than <tt>other</tt>
259: */
260: public int compareTo(Object other) {
261: RSSItem otherItem = (RSSItem) other;
262:
263: Date otherDate = otherItem.getPublicationDate();
264: Date this Date = this .getPublicationDate();
265: Date now = new Date();
266:
267: if (otherDate == null)
268: otherDate = now;
269:
270: if (this Date == null)
271: this Date = now;
272:
273: int cmp = this Date.compareTo(otherDate);
274: if (cmp == 0) {
275: String otherTitle = otherItem.getTitle();
276: String this Title = this .getTitle();
277:
278: if (otherTitle == null)
279: otherTitle = "";
280:
281: if (this Title == null)
282: this Title = "";
283:
284: if ((cmp = this Title.compareTo(otherTitle)) == 0) {
285: String otherID = otherItem.getID();
286: String this ID = this .getID();
287:
288: if (otherID == null)
289: otherID = "";
290:
291: if (this ID == null)
292: this ID = "";
293:
294: if ((cmp = this ID.compareTo(otherID)) == 0)
295: cmp = this .hashCode() - other.hashCode();
296: }
297: }
298:
299: return cmp;
300: }
301:
302: /**
303: * Generate a hash code for this item.
304: *
305: * @return the hash code
306: */
307: @Override
308: public int hashCode() {
309: int hc;
310: String id = getIdentifier();
311:
312: if (id == null)
313: hc = super .hashCode();
314: else
315: hc = id.hashCode();
316:
317: return hc;
318: }
319:
320: /**
321: * Compare this item to some other object for equality.
322: *
323: * @param o the object
324: *
325: * @return <tt>true</tt> if the objects are equal, <tt>false</tt> if not
326: */
327: @Override
328: public boolean equals(Object o) {
329: boolean eq = false;
330:
331: if (o instanceof RSSItem)
332: eq = getIdentifier().equals(((RSSItem) o).getIdentifier());
333:
334: return eq;
335: }
336:
337: /**
338: * Return the string value of the item (which, right now, is its
339: * title).
340: *
341: * @return the title
342: */
343: @Override
344: public String toString() {
345: return getIdentifier();
346: }
347:
348: /*----------------------------------------------------------------------*\
349: Public Abstract Methods
350: \*----------------------------------------------------------------------*/
351:
352: /**
353: * Create a new, empty instance of the underlying concrete
354: * class.
355: *
356: * @param channel the parent channel
357: *
358: * @return the new instance
359: */
360: public abstract RSSItem newInstance(RSSChannel channel);
361:
362: /**
363: * Get the parent channel
364: *
365: * @return the parent channel
366: */
367: public abstract RSSChannel getParentChannel();
368:
369: /**
370: * Get the item's title
371: *
372: * @return the item's title, or null if there isn't one
373: *
374: * @see #setTitle
375: */
376: public abstract String getTitle();
377:
378: /**
379: * Set the item's title
380: *
381: * @param newTitle the item's title, or null if there isn't one
382: *
383: * @see #getTitle
384: */
385: public abstract void setTitle(String newTitle);
386:
387: /**
388: * Get the item's summary (also sometimes called the description or
389: * synopsis).
390: *
391: * @return the summary, or null if not available
392: *
393: * @see #setSummary
394: */
395: public abstract String getSummary();
396:
397: /**
398: * Set the item's summary (also sometimes called the description or
399: * synopsis).
400: *
401: * @param newSummary the summary, or null if not available
402: *
403: * @see #getSummary
404: */
405: public abstract void setSummary(String newSummary);
406:
407: /**
408: * Get the item's author list.
409: *
410: * @return the authors, or null (or an empty <tt>Collection</tt>) if
411: * not available
412: *
413: * @see #addAuthor
414: * @see #clearAuthors
415: * @see #setAuthors
416: */
417: public abstract Collection<String> getAuthors();
418:
419: /**
420: * Add to the item's author list.
421: *
422: * @param author another author string to add
423: *
424: * @see #getAuthors
425: * @see #clearAuthors
426: * @see #setAuthors
427: */
428: public abstract void addAuthor(String author);
429:
430: /**
431: * Clear the authors list.
432: *
433: * @see #getAuthors
434: * @see #addAuthor
435: * @see #setAuthors
436: */
437: public abstract void clearAuthors();
438:
439: /**
440: * Get the item's published links.
441: *
442: * @return the collection of links, or an empty collection
443: *
444: * @see #setLinks
445: */
446: public abstract Collection<RSSLink> getLinks();
447:
448: /**
449: * Set the item's published links.
450: *
451: * @param links the collection of links, or an empty collection (or null)
452: *
453: * @see #getLinks
454: */
455: public abstract void setLinks(Collection<RSSLink> links);
456:
457: /**
458: * Get the categories the item belongs to.
459: *
460: * @return a <tt>Collection</tt> of category strings (<tt>String</tt>
461: * objects) or null if not applicable
462: *
463: * @see #setCategories
464: */
465: public abstract Collection<String> getCategories();
466:
467: /**
468: * Set the categories the item belongs to.
469: *
470: * @param categories a <tt>Collection</tt> of category strings
471: * or null if not applicable
472: *
473: * @see #getCategories
474: */
475: public abstract void setCategories(Collection<String> categories);
476:
477: /**
478: * Get the item's publication date.
479: *
480: * @return the date, or null if not available
481: *
482: * @see #getPublicationDate
483: */
484: public abstract Date getPublicationDate();
485:
486: /**
487: * Set the item's publication date.
488: *
489: * @param date the new date, or null to clear
490: *
491: * @see #getPublicationDate
492: */
493: public abstract void setPublicationDate(Date date);
494:
495: /**
496: * Get the item's ID field, if any.
497: *
498: * @return the ID field, or null if not set
499: *
500: * @see #setID
501: */
502: @Override
503: public abstract String getID();
504:
505: /**
506: * Set the item's ID field, if any.
507: *
508: * @param id the ID field, or null
509: */
510: public abstract void setID(String id);
511:
512: /*----------------------------------------------------------------------*\
513: Protected Methods
514: \*----------------------------------------------------------------------*/
515:
516: /**
517: * Get all content associated with this item.
518: *
519: * @return a <tt>Collection</tt> of {@link RSSContent} objects
520: */
521: protected abstract Collection<RSSContent> getContent();
522:
523: /**
524: * Used by {@link #makeCopy}, this method copies any subclass fields
525: * that aren't visible to this class.
526: *
527: * @param toItem the other {@link RSSItem} into which to copy fields.
528: * <tt>item</tt> will have been created by a call to
529: * {@link #newInstance}
530: */
531: protected abstract void copyPrivateFields(RSSItem toItem);
532:
533: /*----------------------------------------------------------------------*\
534: Private Methods
535: \*----------------------------------------------------------------------*/
536:
537: /**
538: * Get a unique identifier for this RSSItem. This method will return the
539: * ID (see getID()), if it's set; otherwise, it'll return the URL. If
540: * there's no URL, it'll return the title. If there's no title, it returns
541: * null.
542: *
543: * @return a unique identifier
544: */
545: private String getIdentifier() {
546: String id = getID();
547: if (id == null) {
548: RSSLink url = getURL();
549: if (url != null)
550: id = url.toString();
551: else {
552: // No URL. Use the hash code of the title, if present.
553:
554: id = getTitle();
555: }
556: }
557:
558: return id;
559: }
560:
561: /**
562: * Initialize the content map.
563: */
564: private void initContentMap() {
565: if (contentMap == null) {
566: contentMap = new HashMap<String, String>();
567:
568: Collection<RSSContent> content = getContent();
569: if ((content != null) && (content.size() > 0)) {
570: RSSContent first = null;
571: for (RSSContent contentItem : content) {
572: contentMap.put(contentItem.getMIMEType(),
573: contentItem.getTextContent());
574: if (first == null)
575: first = contentItem;
576: }
577:
578: // The default content is the first one.
579:
580: contentMap.put(DEFAULT_CONTENT_TYPE, first
581: .getTextContent());
582: }
583: }
584: }
585: }
|