/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.dspace.content.*;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.CommunityService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.handle.factory.HandleServiceFactory;
import org.w3c.dom.Document;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
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.SyndEnclosure;
import com.sun.syndication.feed.synd.SyndEnclosureImpl;
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.feed.module.itunes.*;
import com.sun.syndication.feed.module.itunes.types.Duration;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.FeedException;
import org.apache.log4j.Logger;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* 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
{
protected 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
protected String defaultTitleField = "dc.title";
protected String defaultAuthorField = "dc.contributor.author";
protected String defaultDateField = "dc.date.issued";
private static final String[] defaultDescriptionFields = new String[]{"dc.description.abstract", "dc.description", "dc.title.alternative", "dc.title"};
protected String defaultExternalMedia = "dc.source.uri";
private final ConfigurationService configurationService =
DSpaceServicesFactory.getInstance().getConfigurationService();
// metadata field for Item title in entry:
protected String titleField =
configurationService.getProperty("webui.feed.item.title", defaultTitleField);
// metadata field for Item publication date in entry:
protected String dateField =
configurationService.getProperty("webui.feed.item.date", defaultDateField);
// metadata field for Item description in entry:
private static final String descriptionFields[] =
DSpaceServicesFactory.getInstance().getConfigurationService().getArrayProperty("webui.feed.item.description", defaultDescriptionFields);
protected String authorField =
configurationService.getProperty("webui.feed.item.author", defaultAuthorField);
// metadata field for Podcast external media source url
protected String externalSourceField =
configurationService.getProperty("webui.feed.podcast.sourceuri", defaultExternalMedia);
// metadata field for Item dc:creator field in entry's DCModule (no default)
protected String dcCreatorField = configurationService.getProperty("webui.feed.item.dc.creator");
// metadata field for Item dc:date field in entry's DCModule (no default)
protected String dcDateField = configurationService.getProperty("webui.feed.item.dc.date");
// metadata field for Item dc:author field in entry's DCModule (no default)
protected String dcDescriptionField = configurationService.getProperty("webui.feed.item.dc.description");
// List of available mimetypes that we'll add to podcast feed. Multiple values separated by commas
protected String[] podcastableMIMETypes =
configurationService.getArrayProperty("webui.feed.podcast.mimetypes", new String[]{"audio/x-mpeg"});
// -------- Instance variables:
// the feed object we are building
protected SyndFeed feed = null;
// memory of UI that called us, "xmlui" or "jspui"
// affects Bitstream retrieval URL and I18N keys
protected String uiType = null;
protected HttpServletRequest request = null;
protected CollectionService collectionService;
protected CommunityService communityService;
protected ItemService itemService;
/**
* Constructor.
* @param ui either "xmlui" or "jspui"
*/
public SyndicationFeed(String ui)
{
feed = new SyndFeedImpl();
uiType = ui;
ContentServiceFactory contentServiceFactory = ContentServiceFactory.getInstance();
itemService = contentServiceFactory.getItemService();
collectionService = contentServiceFactory.getCollectionService();
communityService = contentServiceFactory.getCommunityService();
}
/**
* Returns list of metadata selectors used to compose the description element
*
* @return selector list - format 'schema.element[.qualifier]'
*/
public static String[] getDescriptionSelectors()
{
return (String[]) ArrayUtils.clone(descriptionFields);
}
/**
* Fills in the feed and entry-level metadata from DSpace objects.
* @param request request
* @param context context
* @param dso DSpaceObject
* @param items array of objects
* @param labels label map
*/
public void populate(HttpServletRequest request, Context context, DSpaceObject dso,
List<?extends DSpaceObject> items, Map<String, String> labels)
{
String logoURL = null;
String objectURL = null;
String defaultTitle = null;
boolean podcastFeed = false;
this.request = request;
// 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.getName();
feed.setDescription(collectionService.getMetadata(col, "short_description"));
logo = col.getLogo();
String cols = ConfigurationManager.getProperty("webui.feed.podcast.collections");
if(cols != null && cols.length() > 1 && cols.contains(col.getHandle()) ) {
podcastFeed = true;
}
}
else if (dso.getType() == Constants.COMMUNITY)
{
Community comm = (Community)dso;
defaultTitle = comm.getName();
feed.setDescription(communityService.getMetadata(comm, "short_description"));
logo = comm.getLogo();
String comms = ConfigurationManager.getProperty("webui.feed.podcast.communities");
if(comms != null && comms.length() > 1 && comms.contains(comm.getHandle()) ){
podcastFeed = true;
}
}
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);
if (StringUtils.isNotBlank(feed.getTitle())) {
image.setTitle(feed.getTitle());
} else {
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\\)", "");
}
List<MetadataValue> dcv = itemService.getMetadataByMetadataString(item, df);
if (dcv.size() > 0)
{
String fieldLabel = labels.get(MSG_METADATA + df);
if (fieldLabel != null && fieldLabel.length()>0)
{
db.append(fieldLabel).append(": ");
}
boolean first = true;
for (MetadataValue v : dcv)
{
if (first)
{
first = false;
}
else
{
db.append("; ");
}
db.append(isDate ? new DCDate(v.getValue()).toString() : v.getValue());
}
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
List<MetadataValue> authors = itemService.getMetadataByMetadataString(item, authorField);
if (authors.size() > 0)
{
List<SyndPerson> creators = new ArrayList<SyndPerson>();
for (MetadataValue author : authors)
{
SyndPerson sp = new SyndPersonImpl();
sp.setName(author.getValue());
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)
{
List<MetadataValue> dcAuthors = itemService.getMetadataByMetadataString(item, dcCreatorField);
if (dcAuthors.size() > 0)
{
List<String> creators = new ArrayList<String>();
for (MetadataValue author : dcAuthors)
{
creators.add(author.getValue());
}
dc.setCreators(creators);
}
}
if (dcDateField != null && !hasDate)
{
List<MetadataValue> v = itemService.getMetadataByMetadataString(item, dcDateField);
if (v.size() > 0)
{
dc.setDate((new DCDate(v.get(0).getValue())).toDate());
}
}
if (dcDescriptionField != null)
{
List<MetadataValue> v = itemService.getMetadataByMetadataString(item, dcDescriptionField);
if (v.size() > 0)
{
StringBuffer descs = new StringBuffer();
for (MetadataValue d : v)
{
if (descs.length() > 0)
{
descs.append("\n\n");
}
descs.append(d.getValue());
}
dc.setDescription(descs.toString());
}
}
entry.getModules().add(dc);
}
//iTunes Podcast Support - START
if (podcastFeed)
{
// Add enclosure(s)
List<SyndEnclosure> enclosures = new ArrayList();
try {
List<Bundle> bunds = itemService.getBundles(item, "ORIGINAL");
if (bunds.get(0) != null) {
List<Bitstream> bits = bunds.get(0).getBitstreams();
for (Bitstream bit : bits) {
String mime = bit.getFormat(context).getMIMEType();
if (ArrayUtils.contains(podcastableMIMETypes,mime)) {
SyndEnclosure enc = new SyndEnclosureImpl();
enc.setType(bit.getFormat(context).getMIMEType());
enc.setLength(bit.getSize());
enc.setUrl(urlOfBitstream(request, bit));
enclosures.add(enc);
} else {
continue;
}
}
}
//Also try to add an external value from dc.identifier.other
// We are assuming that if this is set, then it is a media file
List<MetadataValue> externalMedia = itemService.getMetadataByMetadataString(item, externalSourceField);
if(externalMedia.size() > 0)
{
for (MetadataValue anExternalMedia : externalMedia) {
SyndEnclosure enc = new SyndEnclosureImpl();
enc.setType("audio/x-mpeg"); //We can't determine MIME of external file, so just picking one.
enc.setLength(1);
enc.setUrl(anExternalMedia.getValue());
enclosures.add(enc);
}
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
entry.setEnclosures(enclosures);
// Get iTunes specific fields: author, subtitle, summary, duration, keywords
EntryInformation itunes = new EntryInformationImpl();
String author = getOneDC(item, authorField);
if (author != null && author.length() > 0) {
itunes.setAuthor(author); // <itunes:author>
}
itunes.setSubtitle(title == null ? localize(labels, MSG_UNTITLED) : title); // <itunes:subtitle>
if (db.length() > 0) {
itunes.setSummary(db.toString()); // <itunes:summary>
}
String extent = getOneDC(item, "dc.format.extent"); // assumed that user will enter this field with length of song in seconds
if (extent != null && extent.length() > 0) {
extent = extent.split(" ")[0];
Integer duration = Integer.parseInt(extent);
itunes.setDuration(new Duration(duration)); // <itunes:duration>
}
String subject = getOneDC(item, "dc.subject");
if (subject != null && subject.length() > 0) {
String[] subjects = new String[1];
subjects[0] = subject;
itunes.setKeywords(subjects); // <itunes:keywords>
}
entry.getModules().add(itunes);
}
}
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)
* @param feedType feed type
*/
public void setType(String feedType)
{
feed.setFeedType(feedType);
// XXX FIXME: workaround ROME 1.0 bug, it puts invalid image element in rss1.0
if ("rss_1.0".equals(feedType))
{
feed.setImage(null);
}
}
/**
* @return the feed we built as DOM Document
* @throws FeedException if feed error
*/
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
* @throws FeedException if feed error
*/
public String outputString()
throws FeedException
{
SyndFeedOutput feedWriter = new SyndFeedOutput();
return feedWriter.outputString(feed);
}
/**
* send the output to designated Writer
* @param writer Writer
* @throws FeedException if feed error
* @throws IOException if IO error
*/
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.
* @param m module
*/
public void addModule(Module m)
{
feed.getModules().add(m);
}
// utility to get config property with default value when not set.
protected 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)
protected 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);
}
protected String baseURL = null; // cache the result for null
/**
* 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 request current servlet request
* @param dso The object to reference, null if to the repository.
* @return URL
*/
protected 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 HandleServiceFactory.getInstance().getHandleService().getCanonicalForm(dso.getHandle());
}
}
// retrieve text for localization key, or mark untranslated
protected 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
protected String getOneDC(Item item, String field)
{
List<MetadataValue> dcv = itemService.getMetadataByMetadataString(item, field);
return (dcv.size() > 0) ? dcv.get(0).getValue() : null;
}
}