KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > dspace > app > webui > servlet > FeedServlet


1 /*
2  * FeedServlet.java
3  *
4  * Version: $Revision: 1.6 $
5  *
6  * Date: $Date: 2006/11/24 00:44:03 $
7  *
8  * Copyright (c) 2002-2005, Hewlett-Packard Company and Massachusetts
9  * Institute of Technology. All rights reserved.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions are
13  * met:
14  *
15  * - Redistributions of source code must retain the above copyright
16  * notice, this list of conditions and the following disclaimer.
17  *
18  * - Redistributions in binary form must reproduce the above copyright
19  * notice, this list of conditions and the following disclaimer in the
20  * documentation and/or other materials provided with the distribution.
21  *
22  * - Neither the name of the Hewlett-Packard Company nor the name of the
23  * Massachusetts Institute of Technology nor the names of their
24  * contributors may be used to endorse or promote products derived from
25  * this software without specific prior written permission.
26  *
27  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31  * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
32  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
33  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
34  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
35  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
36  * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
37  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
38  * DAMAGE.
39  */

40
41 package org.dspace.app.webui.servlet;
42
43 import java.io.IOException JavaDoc;
44 import java.sql.SQLException JavaDoc;
45 import java.text.MessageFormat JavaDoc;
46 import java.text.ParseException JavaDoc;
47 import java.util.Date JavaDoc;
48 import java.util.Iterator JavaDoc;
49 import java.util.Locale JavaDoc;
50 import java.util.Map JavaDoc;
51 import java.util.HashMap JavaDoc;
52 import java.util.List JavaDoc;
53 import java.util.ArrayList JavaDoc;
54 import java.util.ResourceBundle JavaDoc;
55 import java.util.StringTokenizer JavaDoc;
56
57 import javax.servlet.ServletException JavaDoc;
58 import javax.servlet.http.HttpServletRequest JavaDoc;
59 import javax.servlet.http.HttpServletResponse JavaDoc;
60
61 import org.apache.log4j.Logger;
62
63 import com.sun.syndication.feed.rss.Channel;
64 import com.sun.syndication.feed.rss.Description;
65 import com.sun.syndication.feed.rss.Image;
66 import com.sun.syndication.feed.rss.TextInput;
67 import com.sun.syndication.io.WireFeedOutput;
68 import com.sun.syndication.io.FeedException;
69
70 import org.dspace.app.webui.util.JSPManager;
71 import org.dspace.authorize.AuthorizeException;
72 import org.dspace.content.DSpaceObject;
73 import org.dspace.content.Collection;
74 import org.dspace.content.Community;
75 import org.dspace.content.Item;
76 import org.dspace.content.Bitstream;
77 import org.dspace.content.DCValue;
78 import org.dspace.content.DCDate;
79 import org.dspace.core.LogManager;
80 import org.dspace.core.ConfigurationManager;
81 import org.dspace.core.Constants;
82 import org.dspace.core.Context;
83 import org.dspace.browse.Browse;
84 import org.dspace.browse.BrowseScope;
85 import org.dspace.handle.HandleManager;
86 import org.dspace.search.Harvest;
87
88 /**
89  * Servlet for handling requests for a syndication feed. The Handle of the collection
90  * or community is extracted from the URL, e.g: <code>/feed/rss_1.0/1234/5678</code>.
91  * Currently supports only RSS feed formats.
92  *
93  * @author Ben Bosman, Richard Rodgers
94  * @version $Revision: 1.6 $
95  */

96 public class FeedServlet extends DSpaceServlet
97 {
98     // key for site-wide feed
99
public static final String JavaDoc SITE_FEED_KEY = "site";
100     
101     // one hour in milliseconds
102
private static final long HOUR_MSECS = 60 * 60 * 1000;
103     /** log4j category */
104     private static Logger log = Logger.getLogger(FeedServlet.class);
105     private String JavaDoc clazz = "org.dspace.app.webui.servlet.FeedServlet";
106
107     
108     // are syndication feeds enabled?
109
private static boolean enabled = false;
110     // number of DSpace items per feed
111
private static int itemCount = 0;
112     // optional cache of feeds
113
private static Map JavaDoc feedCache = null;
114     // maximum size of cache - 0 means caching disabled
115
private static int cacheSize = 0;
116     // how many days to keep a feed in cache before checking currency
117
private static int cacheAge = 0;
118     // supported syndication formats
119
private static List JavaDoc formats = null;
120     
121     // localized resource bundle
122
private static ResourceBundle JavaDoc labels = null;
123     
124     //default fields to display in item description
125
private static String JavaDoc defaultDescriptionFields = "dc.title, dc.contributor.author, dc.contributor.editor, dc.description.abstract, dc.description";
126
127     
128     static
129     {
130         enabled = ConfigurationManager.getBooleanProperty("webui.feed.enable");
131     }
132     
133     public void init()
134     {
135         // read rest of config info if enabled
136
if (enabled)
137         {
138             String JavaDoc fmtsStr = ConfigurationManager.getProperty("webui.feed.formats");
139             if ( fmtsStr != null )
140             {
141                 formats = new ArrayList JavaDoc();
142                 String JavaDoc[] fmts = fmtsStr.split(",");
143                 for (int i = 0; i < fmts.length; i++)
144                 {
145                     formats.add(fmts[i]);
146                 }
147             }
148             itemCount = ConfigurationManager.getIntProperty("webui.feed.items");
149             cacheSize = ConfigurationManager.getIntProperty("webui.feed.cache.size");
150             if (cacheSize > 0)
151             {
152                 feedCache = new HashMap JavaDoc();
153                 cacheAge = ConfigurationManager.getIntProperty("webui.feed.cache.age");
154             }
155         }
156     }
157
158     protected void doDSGet(Context context, HttpServletRequest JavaDoc request,
159             HttpServletResponse JavaDoc response) throws ServletException JavaDoc, IOException JavaDoc,
160             SQLException JavaDoc, AuthorizeException
161     {
162         String JavaDoc path = request.getPathInfo();
163         String JavaDoc feedType = null;
164         String JavaDoc handle = null;
165
166         if(labels==null)
167         {
168             // Get access to the localized resource bundle
169
Locale JavaDoc locale = request.getLocale();
170             labels = ResourceBundle.getBundle("Messages", locale);
171         }
172         
173         if (path != null)
174         {
175             // substring(1) is to remove initial '/'
176
path = path.substring(1);
177             int split = path.indexOf("/");
178             if (split != -1)
179             {
180                 feedType = path.substring(0,split);
181                 handle = path.substring(split+1);
182             }
183         }
184
185         DSpaceObject dso = null;
186         
187         //as long as this is not a site wide feed,
188
//attempt to retrieve the Collection or Community object
189
if(!handle.equals(SITE_FEED_KEY))
190         {
191             // Determine if handle is a valid reference
192
dso = HandleManager.resolveToObject(context, handle);
193         }
194         
195         if (! enabled || (dso != null &&
196             (dso.getType() != Constants.COLLECTION && dso.getType() != Constants.COMMUNITY)) )
197         {
198             log.info(LogManager.getHeader(context, "invalid_id", "path=" + path));
199             JSPManager.showInvalidIDError(request, response, path, -1);
200             return;
201         }
202         
203         // Determine if requested format is supported
204
if( feedType == null || ! formats.contains( feedType ) )
205         {
206             log.info(LogManager.getHeader(context, "invalid_syndformat", "path=" + path));
207             JSPManager.showInvalidIDError(request, response, path, -1);
208             return;
209         }
210         
211         // Lookup or generate the feed
212
Channel channel = null;
213         if (feedCache != null)
214         {
215             // Cache key is handle
216
CacheFeed cFeed = (CacheFeed)feedCache.get(handle);
217             if (cFeed != null) // cache hit, but...
218
{
219                 // Is the feed current?
220
boolean cacheFeedCurrent = false;
221                 if (cFeed.timeStamp + (cacheAge * HOUR_MSECS) < System.currentTimeMillis())
222                 {
223                     cacheFeedCurrent = true;
224                 }
225                 // Not current, but have any items changed since feed was created/last checked?
226
else if ( ! itemsChanged(context, dso, cFeed.timeStamp))
227                 {
228                     // no items have changed, re-stamp feed and use it
229
cFeed.timeStamp = System.currentTimeMillis();
230                     cacheFeedCurrent = true;
231                 }
232                 if (cacheFeedCurrent)
233                 {
234                     channel = cFeed.access();
235                 }
236             }
237         }
238         
239         // either not caching, not found in cache, or feed in cache not current
240
if (channel == null)
241         {
242             channel = generateFeed(context, dso);
243             if (feedCache != null)
244             {
245                 cache(handle, new CacheFeed(channel));
246             }
247         }
248         
249         // set the feed to the requested type & return it
250
channel.setFeedType(feedType);
251         WireFeedOutput feedWriter = new WireFeedOutput();
252         try
253         {
254             response.setContentType("text/xml; charset=UTF-8");
255             feedWriter.output(channel, response.getWriter());
256         }
257         catch( FeedException fex )
258         {
259             throw new IOException JavaDoc(fex.getMessage());
260         }
261     }
262        
263     private boolean itemsChanged(Context context, DSpaceObject dso, long timeStamp)
264             throws SQLException JavaDoc
265     {
266         // construct start and end dates
267
DCDate dcStartDate = new DCDate( new Date JavaDoc(timeStamp) );
268         DCDate dcEndDate = new DCDate( new Date JavaDoc(System.currentTimeMillis()) );
269
270         // convert dates to ISO 8601, stripping the time
271
String JavaDoc startDate = dcStartDate.toString().substring(0, 10);
272         String JavaDoc endDate = dcEndDate.toString().substring(0, 10);
273         
274         // this invocation should return a non-empty list if even 1 item has changed
275
try {
276             return (Harvest.harvest(context, dso, startDate, endDate,
277                                 0, 1, false, false, false).size() > 0);
278         }
279         catch (ParseException JavaDoc pe)
280         {
281             // This should never get thrown as we have generated the dates ourselves
282
return false;
283         }
284     }
285     
286     /**
287      * Generate a syndication feed for a collection or community
288      * or community
289      *
290      * @param context the DSpace context object
291      *
292      * @param dso DSpace object - collection or community
293      *
294      * @return an object representing the feed
295      */

296     private Channel generateFeed(Context context, DSpaceObject dso)
297             throws IOException JavaDoc, SQLException JavaDoc
298     {
299         // container-level elements
300
String JavaDoc dspaceUrl = ConfigurationManager.getProperty("dspace.url");
301         String JavaDoc type = null;
302         String JavaDoc description = null;
303         String JavaDoc title = null;
304         Bitstream logo = null;
305         // browse scope
306
BrowseScope scope = new BrowseScope(context);
307         // the feed
308
Channel channel = new Channel();
309         
310         //Special Case: if DSpace Object passed in is null,
311
//generate a feed for the entire DSpace site!
312
if(dso == null)
313         {
314             channel.setTitle(ConfigurationManager.getProperty("dspace.name"));
315             channel.setLink(dspaceUrl);
316             channel.setDescription(labels.getString(clazz + ".general-feed.description"));
317         }
318         else //otherwise, this is a Collection or Community specific feed
319
{
320             if (dso.getType() == Constants.COLLECTION)
321             {
322                 type = labels.getString(clazz + ".feed-type.collection");
323                 Collection col = (Collection)dso;
324                 description = col.getMetadata("short_description");
325                 title = col.getMetadata("name");
326                 logo = col.getLogo();
327                 scope.setScope(col);
328             }
329             else if (dso.getType() == Constants.COMMUNITY)
330             {
331                 type = labels.getString(clazz + ".feed-type.community");
332                 Community comm = (Community)dso;
333                 description = comm.getMetadata("short_description");
334                 title = comm.getMetadata("name");
335                 logo = comm.getLogo();
336                 scope.setScope(comm);
337             }
338             
339             String JavaDoc objectUrl = ConfigurationManager.getBooleanProperty("webui.feed.localresolve")
340                 ? HandleManager.resolveToURL(context, dso.getHandle())
341                 : HandleManager.getCanonicalForm(dso.getHandle());
342             
343             // put in container-level data
344
channel.setDescription(description);
345             channel.setLink(objectUrl);
346             //build channel title by passing in type and title
347
String JavaDoc channelTitle = MessageFormat.format(labels.getString(clazz + ".feed.title"),
348                                                         new Object JavaDoc[]{type, title});
349             channel.setTitle(channelTitle);
350             
351             //if collection or community has a logo
352
if (logo != null)
353             {
354                 // we use the path to the logo for this, the logo itself cannot
355
// be contained in the rdf. Not all RSS-viewers show this logo.
356
Image image = new Image();
357                 image.setLink(objectUrl);
358                 image.setTitle(labels.getString(clazz + ".logo.title"));
359                 image.setUrl(dspaceUrl + "/retrieve/" + logo.getID());
360                 channel.setImage(image);
361             }
362         }
363         
364         // this is a direct link to the search-engine of dspace. It searches
365
// in the current collection. Since the current version of DSpace
366
// can't search within collections anymore, this works only in older
367
// version until this bug is fixed.
368
TextInput input = new TextInput();
369         input.setLink(dspaceUrl + "/simple-search");
370         input.setDescription( labels.getString(clazz + ".search.description") );
371         
372         String JavaDoc searchTitle = "";
373         
374         //if a "type" of feed was specified, build search title off that
375
if(type!=null)
376         {
377             searchTitle = MessageFormat.format(labels.getString(clazz + ".search.title"),
378                                         new Object JavaDoc[]{type});
379         }
380         else //otherwise, default to a more generic search title
381
{
382             searchTitle = labels.getString(clazz + ".search.title.default");
383         }
384         
385         input.setTitle(searchTitle);
386         input.setName(labels.getString(clazz + ".search.name"));
387         channel.setTextInput(input);
388                 
389         // gather & add items to the feed.
390
scope.setTotal(itemCount);
391         List JavaDoc results = Browse.getLastSubmitted(scope);
392         List JavaDoc items = new ArrayList JavaDoc();
393         for ( int i = 0; i < results.size(); i++ )
394         {
395             items.add( itemFromDSpaceItem(context, (Item)results.get(i)) );
396         }
397         channel.setItems(items);
398         
399         return channel;
400     }
401     
402     /**
403      * The metadata fields of the given item will be added to the given feed.
404      *
405      * @param context DSpace context object
406      *
407      * @param dspaceItem DSpace Item
408      *
409      * @return an object representing a feed entry
410      */

411     private com.sun.syndication.feed.rss.Item itemFromDSpaceItem(Context context,
412                                                                  Item dspaceItem)
413         throws SQLException JavaDoc
414     {
415         com.sun.syndication.feed.rss.Item rssItem =
416             new com.sun.syndication.feed.rss.Item();
417         
418         //get the title and date fields
419
String JavaDoc titleField = ConfigurationManager.getProperty("webui.feed.item.title");
420         if (titleField == null)
421         {
422             titleField = "dc.title";
423         }
424         
425         String JavaDoc dateField = ConfigurationManager.getProperty("webui.feed.item.date");
426         if (dateField == null)
427         {
428             dateField = "dc.date.issued";
429         }
430         
431         //Set item handle
432
String JavaDoc itHandle = ConfigurationManager.getBooleanProperty("webui.feed.localresolve")
433         ? HandleManager.resolveToURL(context, dspaceItem.getHandle())
434         : HandleManager.getCanonicalForm(dspaceItem.getHandle());
435
436         rssItem.setLink(itHandle);
437         
438         //get first title
439
String JavaDoc title = null;
440         try
441         {
442             title = dspaceItem.getMetadata(titleField)[0].value;
443            
444         }
445         catch (ArrayIndexOutOfBoundsException JavaDoc e)
446         {
447             title = labels.getString(clazz + ".notitle");
448         }
449         rssItem.setTitle(title);
450         
451         // We put some metadata in the description field. This field is
452
// displayed by most RSS viewers
453
String JavaDoc descriptionFields = ConfigurationManager
454                                         .getProperty("webui.feed.item.description");
455
456         if (descriptionFields == null)
457         {
458             descriptionFields = defaultDescriptionFields;
459         }
460         
461         //loop through all the metadata fields to put in the description
462
StringBuffer JavaDoc descBuf = new StringBuffer JavaDoc();
463         StringTokenizer JavaDoc st = new StringTokenizer JavaDoc(descriptionFields, ",");
464
465         while (st.hasMoreTokens())
466         {
467             String JavaDoc field = st.nextToken().trim();
468             boolean isDate = false;
469          
470             // Find out if the field should rendered as a date
471
if (field.indexOf("(date)") > 0)
472             {
473                 field = field.replaceAll("\\(date\\)", "");
474                 isDate = true;
475             }
476
477             
478             //print out this field, along with its value(s)
479
DCValue[] values = dspaceItem.getMetadata(field);
480            
481             if(values != null && values.length>0)
482             {
483                 //as long as there is already something in the description
484
//buffer, print out a few line breaks before the next field
485
if(descBuf.length() > 0)
486                 {
487                     descBuf.append("\n<br/>");
488                     descBuf.append("\n<br/>");
489                 }
490                     
491                 String JavaDoc fieldLabel = null;
492                 try
493                 {
494                     fieldLabel = labels.getString("metadata." + field);
495                 }
496                 catch(java.util.MissingResourceException JavaDoc e) {}
497                 
498                 if(fieldLabel !=null && fieldLabel.length()>0)
499                     descBuf.append(fieldLabel + ": ");
500                 
501                 for(int i=0; i<values.length; i++)
502                 {
503                     String JavaDoc fieldValue = values[i].value;
504                     if(isDate)
505                         fieldValue = (new DCDate(fieldValue)).toString();
506                     descBuf.append(fieldValue);
507                     if (i < values.length - 1)
508                     {
509                         descBuf.append("; ");
510                     }
511                 }
512             }
513             
514         }//end while
515
Description descrip = new Description();
516         descrip.setValue(descBuf.toString());
517         rssItem.setDescription(descrip);
518             
519         
520         // set date field
521
String JavaDoc dcDate = null;
522         try
523         {
524             dcDate = dspaceItem.getMetadata(dateField)[0].value;
525            
526         }
527         catch (ArrayIndexOutOfBoundsException JavaDoc e)
528         {
529         }
530         if (dcDate != null)
531         {
532             rssItem.setPubDate((new DCDate(dcDate)).toDate());
533         }
534         
535         return rssItem;
536     }
537
538     /************************************************
539      * private cache management classes and methods *
540      ************************************************/

541      
542     /**
543      * Add a feed to the cache - reducing the size of the cache by 1 to make room if
544      * necessary. The removed entry has an access count equal to the minumum in the cache.
545      * @param feedKey
546      * The cache key for the feed
547      * @param newFeed
548      * The CacheFeed feed to be cached
549      */

550     private static void cache(String JavaDoc feedKey, CacheFeed newFeed)
551     {
552         // remove older feed to make room if cache full
553
if (feedCache.size() >= cacheSize)
554         {
555             // cache profiling data
556
int total = 0;
557             String JavaDoc minKey = null;
558             CacheFeed minFeed = null;
559             CacheFeed maxFeed = null;
560             
561             Iterator JavaDoc iter = feedCache.keySet().iterator();
562             while (iter.hasNext())
563             {
564                 String JavaDoc key = (String JavaDoc)iter.next();
565                 CacheFeed feed = (CacheFeed)feedCache.get(key);
566                 if (minKey != null)
567                 {
568                     if (feed.hits < minFeed.hits)
569                     {
570                         minKey = key;
571                         minFeed = feed;
572                     }
573                     if (feed.hits >= maxFeed.hits)
574                     {
575                         maxFeed = feed;
576                     }
577                 }
578                 else
579                 {
580                     minKey = key;
581                     minFeed = maxFeed = feed;
582                 }
583                 total += feed.hits;
584             }
585             // log a profile of the cache to assist administrator in tuning it
586
int avg = total / feedCache.size();
587             String JavaDoc logMsg = "feedCache() - size: " + feedCache.size() +
588                             " Hits - total: " + total + " avg: " + avg +
589                             " max: " + maxFeed.hits + " min: " + minFeed.hits;
590             log.info(logMsg);
591             // remove minimum hits entry
592
feedCache.remove(minKey);
593         }
594         // add feed to cache
595
feedCache.put(feedKey, newFeed);
596     }
597         
598     /**
599      * Class to instrument accesses & currency of a given feed in cache
600      */

601     private class CacheFeed
602     {
603         // currency timestamp
604
public long timeStamp = 0L;
605         // access count
606
public int hits = 0;
607         // the feed
608
private Channel feed = null;
609         
610         public CacheFeed(Channel feed)
611         {
612             this.feed = feed;
613             timeStamp = System.currentTimeMillis();
614         }
615                 
616         public Channel access()
617         {
618             ++hits;
619             return feed;
620         }
621     }
622 }
623
Popular Tags