001: /*---------------------------------------------------------------------------*\
002: $Id: SortArticlesPlugIn.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.plugins;
047:
048: import org.clapper.curn.Constants;
049: import org.clapper.curn.CurnConfig;
050: import org.clapper.curn.CurnException;
051: import org.clapper.curn.FeedInfo;
052: import org.clapper.curn.FeedConfigItemPlugIn;
053: import org.clapper.curn.MainConfigItemPlugIn;
054: import org.clapper.curn.PostFeedParsePlugIn;
055: import org.clapper.curn.parser.RSSChannel;
056: import org.clapper.curn.parser.RSSItem;
057:
058: import org.clapper.util.classutil.ClassUtil;
059: import org.clapper.util.config.ConfigurationException;
060: import org.clapper.util.logging.Logger;
061:
062: import java.net.URL;
063:
064: import java.util.ArrayList;
065: import java.util.Collection;
066: import java.util.Collections;
067: import java.util.Comparator;
068: import java.util.Date;
069: import java.util.List;
070: import java.util.Map;
071: import java.util.HashMap;
072: import org.clapper.curn.FeedCache;
073:
074: /**
075: * The <tt>SortArticlesPlugIn</tt> handles per-feed SortBy settings.
076: * It looks for a default (main-configuration section) "SortBy" parameter,
077: * and permits a per-feed "SortBy" parameter to override the default.
078: *
079: * <table border="1">
080: * <tr valign="top">
081: * <td><tt>SortBy</tt></td>
082: * <td>Criteria by which to sort. Legal values are:
083: * <ul>
084: * <li><tt>none</tt> — Leave items in whatever order they
085: * appear in the feed.
086: * <li><tt>time</tt> — Sort items by timestamp
087: * <li><tt>title</tt> — Sort items by title
088: * </td>
089: * </tr>
090: * </table>
091: *
092: * @version <tt>$Revision: 7041 $</tt>
093: */
094: public class SortArticlesPlugIn implements MainConfigItemPlugIn,
095: FeedConfigItemPlugIn, PostFeedParsePlugIn {
096: /*----------------------------------------------------------------------*\
097: Private Constants
098: \*----------------------------------------------------------------------*/
099:
100: private static final String VAR_SORT_BY = "SortBy";
101: private static final SortBy DEF_SORT_BY = SortBy.NONE;
102:
103: private enum SortBy {
104: NONE, TIME, TITLE
105: };
106:
107: /**
108: * Legal values
109: */
110: private static final Map<String, SortBy> LEGAL_SORT_BY_VALUES = new HashMap<String, SortBy>();
111: static {
112: LEGAL_SORT_BY_VALUES.put("none", SortBy.NONE);
113: LEGAL_SORT_BY_VALUES.put("time", SortBy.TIME);
114: LEGAL_SORT_BY_VALUES.put("title", SortBy.TITLE);
115: }
116:
117: /*----------------------------------------------------------------------*\
118: Private Classes
119: \*----------------------------------------------------------------------*/
120:
121: private class ItemComparator implements Comparator<RSSItem> {
122: private Date now = new Date();
123: private SortBy sortBy;
124:
125: ItemComparator(SortBy sortBy) {
126: this .sortBy = sortBy;
127: }
128:
129: public int compare(RSSItem i1, RSSItem i2) {
130: int cmp = 0;
131:
132: switch (sortBy) {
133: case TITLE:
134: String title1 = i1.getTitle();
135: if (title1 == null)
136: title1 = "";
137:
138: String title2 = i2.getTitle();
139: if (title2 == null)
140: title2 = "";
141:
142: cmp = title1.compareToIgnoreCase(title2);
143: break;
144:
145: case TIME:
146: Date time1 = i1.getPublicationDate();
147: if (time1 == null)
148: time1 = now;
149:
150: Date time2 = i2.getPublicationDate();
151: if (time2 == null)
152: time2 = now;
153:
154: cmp = time1.compareTo(time2);
155: break;
156:
157: default:
158: cmp = -1;
159: break;
160: }
161:
162: return cmp;
163: }
164:
165: @Override
166: public int hashCode() {
167: return super .hashCode();
168: }
169:
170: @Override
171: public boolean equals(Object o) {
172: return (o instanceof ItemComparator);
173: }
174: }
175:
176: /*----------------------------------------------------------------------*\
177: Private Data Items
178: \*----------------------------------------------------------------------*/
179:
180: /**
181: * Feed sort-by data, by feed
182: */
183: private Map<URL, SortBy> perFeedSortByMap = new HashMap<URL, SortBy>();
184:
185: /**
186: * Default sort-by value
187: */
188: private SortBy defaultSortBy = DEF_SORT_BY;
189: /**
190: * For log messages
191: */
192: private static final Logger log = new Logger(
193: SortArticlesPlugIn.class);
194:
195: /*----------------------------------------------------------------------*\
196: Constructor
197: \*----------------------------------------------------------------------*/
198:
199: /**
200: * Default constructor (required).
201: */
202: public SortArticlesPlugIn() {
203: // Nothing to do
204: }
205:
206: /*----------------------------------------------------------------------*\
207: Public Methods Required by *PlugIn Interfaces
208: \*----------------------------------------------------------------------*/
209:
210: /**
211: * Get a displayable name for the plug-in.
212: *
213: * @return the name
214: */
215: public String getPlugInName() {
216: return "Sort Articles";
217: }
218:
219: /**
220: * Get the sort key for this plug-in.
221: *
222: * @return the sort key string.
223: */
224: public String getPlugInSortKey() {
225: return ClassUtil.getShortClassName(getClass().getName());
226: }
227:
228: /**
229: * Initialize the plug-in. This method is called before any of the
230: * plug-in methods are called.
231: *
232: * @throws CurnException on error
233: */
234: public void initPlugIn() throws CurnException {
235: }
236:
237: /**
238: * Called immediately after <i>curn</i> has read and processed a
239: * configuration item in the main [curn] configuration section. All
240: * configuration items are passed, one by one, to each loaded plug-in.
241: * If a plug-in class is not interested in a particular configuration
242: * item, this method should simply return without doing anything. Note
243: * that some configuration items may simply be variable assignment;
244: * there's no real way to distinguish a variable assignment from a
245: * blessed configuration item.
246: *
247: * @param sectionName the name of the configuration section where
248: * the item was found
249: * @param paramName the name of the parameter
250: * @param config the {@link CurnConfig} object
251: *
252: * @throws CurnException on error
253: *
254: * @see CurnConfig
255: */
256: public void runMainConfigItemPlugIn(String sectionName,
257: String paramName, CurnConfig config) throws CurnException {
258: try {
259: if (paramName.equals(VAR_SORT_BY)) {
260: String val = config.getOptionalStringValue(sectionName,
261: paramName, null);
262: defaultSortBy = (val == null) ? DEF_SORT_BY
263: : parseSortByValue(sectionName, val);
264: }
265: }
266:
267: catch (ConfigurationException ex) {
268: throw new CurnException(ex);
269: }
270: }
271:
272: /**
273: * Called immediately after <i>curn</i> has read and processed a
274: * configuration item in a "feed" configuration section. All
275: * configuration items are passed, one by one, to each loaded plug-in.
276: * If a plug-in class is not interested in a particular configuration
277: * item, this method should simply return without doing anything. Note
278: * that some configuration items may simply be variable assignment;
279: * there's no real way to distinguish a variable assignment from a
280: * blessed configuration item.
281: *
282: * @param sectionName the name of the configuration section where
283: * the item was found
284: * @param paramName the name of the parameter
285: * @param config the active configuration
286: * @param feedInfo partially complete <tt>FeedInfo</tt> object
287: * for the feed. The URL is guaranteed to be
288: * present, but no other fields are.
289: *
290: * @return <tt>true</tt> to continue processing the feed,
291: * <tt>false</tt> to skip it
292: *
293: * @throws CurnException on error
294: *
295: * @see CurnConfig
296: * @see FeedInfo
297: * @see FeedInfo#getURL
298: */
299: public boolean runFeedConfigItemPlugIn(String sectionName,
300: String paramName, CurnConfig config, FeedInfo feedInfo)
301: throws CurnException {
302: try {
303: if (paramName.equals(VAR_SORT_BY)) {
304: String value = config.getConfigurationValue(
305: sectionName, paramName);
306: SortBy sortBy = parseSortByValue(sectionName, value);
307: URL feedURL = feedInfo.getURL();
308: perFeedSortByMap.put(feedURL, sortBy);
309: log.debug(feedURL + ": SortBy=" + sortBy);
310: }
311:
312: return true;
313: }
314:
315: catch (ConfigurationException ex) {
316: throw new CurnException(ex);
317: }
318: }
319:
320: /**
321: * Called immediately after a feed is parsed, but before it is
322: * otherwise processed. This method can return <tt>false</tt> to signal
323: * <i>curn</i> that the feed should be skipped. For instance, a plug-in
324: * that filters on the parsed feed data could use this method to weed
325: * out non-matching feeds before they are downloaded. Similarly, a
326: * plug-in that edits the parsed data (removing or editing individual
327: * items, for instance) could use method to do so.
328: *
329: * @param feedInfo the {@link FeedInfo} object for the feed that
330: * has been downloaded and parsed.
331: * @param feedCache the feed cache
332: * @param channel the parsed channel data
333: *
334: * @return <tt>true</tt> if <i>curn</i> should continue to process the
335: * feed, <tt>false</tt> to skip the feed. A return value of
336: * <tt>false</tt> aborts all further processing on the feed.
337: * In particular, <i>curn</i> will not pass the feed along to
338: * other plug-ins that have yet to be notified of this event.
339: *
340: * @throws CurnException on error
341: *
342: * @see RSSChannel
343: * @see FeedInfo
344: */
345: public boolean runPostFeedParsePlugIn(FeedInfo feedInfo,
346: FeedCache feedCache, RSSChannel channel)
347: throws CurnException {
348: log.debug("Post feed parse: " + feedInfo.getURL());
349: channel
350: .setItems(sortChannelItems(channel.getItems(), feedInfo));
351: return true;
352: }
353:
354: /*----------------------------------------------------------------------*\
355: Private Methods
356: \*----------------------------------------------------------------------*/
357:
358: /**
359: * Parse a "SortBy" value.
360: *
361: * @param sectionName section name (for error messages)
362: * @param value the value from the config
363: *
364: * @return the value, or the appropriate default
365: *
366: * @throws ConfigurationException bad value for config item
367: */
368: private SortBy parseSortByValue(String sectionName, String value)
369: throws ConfigurationException {
370: SortBy val = LEGAL_SORT_BY_VALUES.get(value);
371:
372: if (val == null) {
373: throw new ConfigurationException(
374: Constants.BUNDLE_NAME,
375: "CurnConfig.badVarValue",
376: "Section \"{0}\" in the configuration file has a bad "
377: + "value (\"{1}\") for the \"{2}\" parameter",
378: new Object[] { sectionName, value, VAR_SORT_BY });
379: }
380:
381: return val;
382: }
383:
384: /**
385: * Sort downloaded items according to the sort criteria for the feed
386: *
387: * @param items the downloaded items
388: * @param feedInfo info about the feed, used to determine the desired
389: * sort criteria
390: *
391: * @return a <tt>Collection</tt> of the same items, possibly sorted
392: */
393: private Collection<RSSItem> sortChannelItems(
394: Collection<RSSItem> items, FeedInfo feedInfo) {
395: Collection<RSSItem> result = null;
396: int total = items.size();
397: URL feedURL = feedInfo.getURL();
398:
399: log.debug("Feed " + feedURL + ": total items=" + total);
400: if (total > 0) {
401: SortBy sortBy = perFeedSortByMap.get(feedURL);
402: log.debug("feed " + feedURL + ": SortBy=" + sortBy);
403: if (sortBy == null)
404: sortBy = defaultSortBy;
405:
406: switch (sortBy) {
407: case NONE:
408: result = new ArrayList<RSSItem>(items);
409: break;
410:
411: case TITLE:
412: case TIME:
413:
414: // Can't just use a TreeSet, with a Comparator, because
415: // then items with the same title will be weeded out.
416:
417: List<RSSItem> newItems = new ArrayList<RSSItem>(items);
418: Collections.sort(newItems, new ItemComparator(sortBy));
419: result = newItems;
420: break;
421:
422: default:
423: assert (false);
424: }
425: }
426:
427: return result;
428: }
429:
430: }
|