/*
* SyndicationFeed.java
*
* Version: $Revision: 1.1 $
*
* Date: $Date: 2009/10/19 21:51:54 $
*
* Copyright (c) 2002-2009, The DSpace Foundation. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of the DSpace Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
package org.dspace.app.util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.w3c.dom.Document;
import org.dspace.content.Bitstream;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DCDate;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataSchema;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.handle.HandleManager;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndImage;
import com.sun.syndication.feed.synd.SyndImageImpl;
import com.sun.syndication.feed.synd.SyndPerson;
import com.sun.syndication.feed.synd.SyndPersonImpl;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.module.DCModuleImpl;
import com.sun.syndication.feed.module.DCModule;
import com.sun.syndication.feed.module.Module;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.FeedException;
import org.apache.log4j.Logger;
/**
* Invoke ROME library to assemble a generic model of a syndication
* for the given list of Items and scope. Consults configuration for the
* metadata bindings to feed elements. Uses ROME's output drivers to
* return any of the implemented formats, e.g. RSS 1.0, RSS 2.0, ATOM 1.0.
*
* The feed generator and OpenSearch call on this class so feed contents are
* uniform for both.
*
* @author Larry Stone
*/
public class SyndicationFeed
{
private static final Logger log = Logger.getLogger(SyndicationFeed.class);
/** i18n key values */
public static final String MSG_UNTITLED = "notitle";
public static final String MSG_LOGO_TITLE = "logo.title";
public static final String MSG_FEED_TITLE = "feed.title";
public static final String MSG_FEED_DESCRIPTION = "general-feed.description";
public static final String MSG_METADATA = "metadata.";
public static final String MSG_UITYPE = "ui.type";
// UI keywords
public static final String UITYPE_XMLUI = "xmlui";
public static final String UITYPE_JSPUI = "jspui";
// default DC fields for entry
private static String defaultTitleField = "dc.title";
private static String defaultAuthorField = "dc.contributor.author";
private static String defaultDateField = "dc.date.issued";
private static String defaultDescriptionFields = "dc.description.abstract, dc.description, dc.title.alternative, dc.title";
// metadata field for Item title in entry:
private static String titleField =
getDefaultedConfiguration("webui.feed.item.title", defaultTitleField);
// metadata field for Item publication date in entry:
private static String dateField =
getDefaultedConfiguration("webui.feed.item.date", defaultDateField);
// metadata field for Item description in entry:
private static String descriptionFields[] =
getDefaultedConfiguration("webui.feed.item.description", defaultDescriptionFields).split("\\s*,\\s*");
private static String authorField =
getDefaultedConfiguration("webui.feed.item.author", defaultAuthorField);
// metadata field for Item dc:creator field in entry's DCModule (no default)
private static String dcCreatorField = ConfigurationManager.getProperty("webui.feed.item.dc.creator");
// metadata field for Item dc:date field in entry's DCModule (no default)
private static String dcDateField = ConfigurationManager.getProperty("webui.feed.item.dc.date");
// metadata field for Item dc:author field in entry's DCModule (no default)
private static String dcDescriptionField = ConfigurationManager.getProperty("webui.feed.item.dc.description");
// -------- Instance variables:
// the feed object we are building
private SyndFeed feed = null;
// memory of UI that called us, "xmlui" or "jspui"
// affects Bitstream retrieval URL and I18N keys
private String uiType = null;
/**
* Constructor.
* @param ui either "xmlui" or "jspui"
*/
public SyndicationFeed(String ui)
{
feed = new SyndFeedImpl();
uiType = ui;
}
/**
* Returns list of metadata selectors used to compose the description element
*
* @return selector list - format 'schema.element[.qualifier]'
*/
public static String[] getDescriptionSelectors()
{
return descriptionFields;
}
/**
* Fills in the feed and entry-level metadata from DSpace objects.
*/
public void populate(HttpServletRequest request, DSpaceObject dso,
DSpaceObject items[], Map<String, String> labels)
{
String logoURL = null;
String objectURL = null;
String defaultTitle = null;
// dso is null for the whole site, or a search without scope
if (dso == null)
{
defaultTitle = ConfigurationManager.getProperty("dspace.name");
feed.setDescription(localize(labels, MSG_FEED_DESCRIPTION));
objectURL = resolveURL(request, null);
logoURL = ConfigurationManager.getProperty("webui.feed.logo.url");
}
else
{
Bitstream logo = null;
if (dso.getType() == Constants.COLLECTION)
{
Collection col = (Collection)dso;
defaultTitle = col.getMetadata("name");
feed.setDescription(col.getMetadata("short_description"));
logo = col.getLogo();
}
else if (dso.getType() == Constants.COMMUNITY)
{
Community comm = (Community)dso;
defaultTitle = comm.getMetadata("name");
feed.setDescription(comm.getMetadata("short_description"));
logo = comm.getLogo();
}
objectURL = resolveURL(request, dso);
if (logo != null)
logoURL = urlOfBitstream(request, logo);
}
feed.setTitle(labels.containsKey(MSG_FEED_TITLE) ?
localize(labels, MSG_FEED_TITLE) : defaultTitle);
feed.setLink(objectURL);
feed.setPublishedDate(new Date());
feed.setUri(objectURL);
// add logo if we found one:
if (logoURL != null)
{
// we use the path to the logo for this, the logo itself cannot
// be contained in the rdf. Not all RSS-viewers show this logo.
SyndImage image = new SyndImageImpl();
image.setLink(objectURL);
image.setTitle(localize(labels, MSG_LOGO_TITLE));
image.setUrl(logoURL);
feed.setImage(image);
}
// add entries for items
if (items != null)
{
List<SyndEntry> entries = new ArrayList<SyndEntry>();
for (DSpaceObject itemDSO : items)
{
if (itemDSO.getType() != Constants.ITEM)
continue;
Item item = (Item)itemDSO;
boolean hasDate = false;
SyndEntry entry = new SyndEntryImpl();
entries.add(entry);
String entryURL = resolveURL(request, item);
entry.setLink(entryURL);
entry.setUri(entryURL);
String title = getOneDC(item, titleField);
entry.setTitle(title == null ? localize(labels, MSG_UNTITLED) : title);
// "published" date -- should be dc.date.issued
String pubDate = getOneDC(item, dateField);
if (pubDate != null)
{
entry.setPublishedDate((new DCDate(pubDate)).toDate());
hasDate = true;
}
// date of last change to Item
entry.setUpdatedDate(item.getLastModified());
StringBuffer db = new StringBuffer();
for (String df : descriptionFields)
{
// Special Case: "(date)" in field name means render as date
boolean isDate = df.indexOf("(date)") > 0;
if (isDate)
df = df.replaceAll("\\(date\\)", "");
DCValue dcv[] = item.getMetadata(df);
if (dcv.length > 0)
{
String fieldLabel = labels.get(MSG_METADATA + df);
if (fieldLabel != null && fieldLabel.length()>0)
db.append(fieldLabel + ": ");
boolean first = true;
for (DCValue v : dcv)
{
if (first)
first = false;
else
db.append("; ");
db.append(isDate ? new DCDate(v.value).toString() : v.value);
}
db.append("\n");
}
}
if (db.length() > 0)
{
SyndContent desc = new SyndContentImpl();
desc.setType("text/plain");
desc.setValue(db.toString());
entry.setDescription(desc);
}
// This gets the authors into an ATOM feed
DCValue authors[] = item.getMetadata(authorField);
if (authors.length > 0)
{
List<SyndPerson> creators = new ArrayList<SyndPerson>();
for (DCValue author : authors)
{
SyndPerson sp = new SyndPersonImpl();
sp.setName(author.value);
creators.add(sp);
}
entry.setAuthors(creators);
}
// only add DC module if any DC fields are configured
if (dcCreatorField != null || dcDateField != null ||
dcDescriptionField != null)
{
DCModule dc = new DCModuleImpl();
if (dcCreatorField != null)
{
DCValue dcAuthors[] = item.getMetadata(dcCreatorField);
if (dcAuthors.length > 0)
{
List<String> creators = new ArrayList<String>();
for (DCValue author : dcAuthors)
creators.add(author.value);
dc.setCreators(creators);
}
}
if (dcDateField != null && !hasDate)
{
DCValue v[] = item.getMetadata(dcDateField);
if (v.length > 0)
dc.setDate((new DCDate(v[0].value)).toDate());
}
if (dcDescriptionField != null)
{
DCValue v[] = item.getMetadata(dcDescriptionField);
if (v.length > 0)
{
StringBuffer descs = new StringBuffer();
for (DCValue d : v)
{
if (descs.length() > 0)
descs.append("\n\n");
descs.append(d.value);
}
dc.setDescription(descs.toString());
}
}
entry.getModules().add(dc);
}
}
feed.setEntries(entries);
}
}
/**
* Sets the feed type for XML delivery, e.g. "rss_1.0", "atom_1.0"
* Must match one of ROME's configured generators, see rome.properties
* (currently rss_1.0, rss_2.0, atom_1.0, atom_0.3)
*/
public void setType(String feedType)
{
feed.setFeedType(feedType);
// XXX FIXME: workaround ROME 1.0 bug, it puts invalid image element in rss1.0
if (feedType.equals("rss_1.0"))
feed.setImage(null);
}
/**
* @return the feed we built as DOM Document
*/
public Document outputW3CDom()
throws FeedException
{
try
{
SyndFeedOutput feedWriter = new SyndFeedOutput();
return feedWriter.outputW3CDom(feed);
}
catch (FeedException e)
{
log.error(e);
throw e;
}
}
/**
* @return the feed we built as serialized XML string
*/
public String outputString()
throws FeedException
{
SyndFeedOutput feedWriter = new SyndFeedOutput();
return feedWriter.outputString(feed);
}
/**
* send the output to designated Writer
*/
public void output(java.io.Writer writer)
throws FeedException, IOException
{
SyndFeedOutput feedWriter = new SyndFeedOutput();
feedWriter.output(feed, writer);
}
/**
* @add a ROME plugin module (e.g. for OpenSearch) at the feed level
*/
public void addModule(Module m)
{
feed.getModules().add(m);
}
// utility to get config property with default value when not set.
private static String getDefaultedConfiguration(String key, String dfl)
{
String result = ConfigurationManager.getProperty(key);
return (result == null) ? dfl : result;
}
// returns absolute URL to download content of bitstream (which might not belong to any Item)
private String urlOfBitstream(HttpServletRequest request, Bitstream logo)
{
String name = logo.getName();
return resolveURL(request,null) +
(uiType.equalsIgnoreCase(UITYPE_XMLUI) ?"/bitstream/id/":"/retrieve/") +
logo.getID()+"/"+(name == null?"":name);
}
/**
* Return a url to the DSpace object, either use the official
* handle for the item or build a url based upon the current server.
*
* If the dspaceobject is null then a local url to the repository is generated.
*
* @param dso The object to refrence, null if to the repository.
* @return
*/
private String baseURL = null; // cache the result for null
private String resolveURL(HttpServletRequest request, DSpaceObject dso)
{
// If no object given then just link to the whole repository,
// since no offical handle exists so we have to use local resolution.
if (dso == null)
{
if (baseURL == null)
{
if (request == null)
baseURL = ConfigurationManager.getProperty("dspace.url");
else
{
baseURL = (request.isSecure()) ? "https://" : "http://";
baseURL += ConfigurationManager.getProperty("dspace.hostname");
baseURL += ":" + request.getServerPort();
baseURL += request.getContextPath();
}
}
return baseURL;
}
// return a link to handle in repository
else if (ConfigurationManager.getBooleanProperty("webui.feed.localresolve"))
{
return resolveURL(request, null) + "/handle/" + dso.getHandle();
}
// link to the Handle server or other persistent URL source
else
{
return HandleManager.getCanonicalForm(dso.getHandle());
}
}
// retrieve text for localization key, or mark untranslated
private String localize(Map<String, String> labels, String s)
{
return labels.containsKey(s) ? labels.get(s) : ("Untranslated:"+s);
}
// spoonful of syntactic sugar when we only need first value
private String getOneDC(Item item, String field)
{
DCValue dcv[] = item.getMetadata(field);
return (dcv.length > 0) ? dcv[0].value : null;
}
}