/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.feed.impl;
import org.opencastproject.feed.api.Content;
import org.opencastproject.feed.api.Content.Mode;
import org.opencastproject.feed.api.Enclosure;
import org.opencastproject.feed.api.Feed;
import org.opencastproject.feed.api.FeedEntry;
import org.opencastproject.feed.api.FeedExtension;
import org.opencastproject.feed.api.FeedGenerator;
import org.opencastproject.mediapackage.AudioStream;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.VideoStream;
import org.opencastproject.mediapackage.track.TrackImpl;
import org.opencastproject.search.api.SearchQuery;
import org.opencastproject.search.api.SearchResult;
import org.opencastproject.search.api.SearchResultItem;
import org.opencastproject.search.api.SearchResultItem.SearchResultItemType;
import org.opencastproject.security.api.Organization;
import org.opencastproject.systems.MatterhornConstants;
import org.opencastproject.util.UrlSupport;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
/**
* This class provides basic functionality for creating feeds and is used as the base implementation for the default
* feed generators.
*/
public abstract class AbstractFeedGenerator implements FeedGenerator {
/** the logging facility provided by log4j */
private static final Logger logger = LoggerFactory.getLogger(AbstractFeedGenerator.class);
/** Property key for the organizations engage ui url */
public static final String PROP_ORG_ENGAGE_UI_URL = "org.opencastproject.engage.ui.url";
/** Property key for the organizations feed url */
public static final String PROP_ORG_FEED_URL = "org.opencastproject.feed.url";
/** Property key for the feed uri */
public static final String PROP_URI = "feed.uri";
/** Property key for the number of feed entries */
public static final String PROP_SIZE = "feed.size";
/** Property key for the feed selector pattern */
public static final String PROP_SELECTOR = "feed.selector";
/** Property key for the feed name */
public static final String PROP_NAME = "feed.name";
/** Property key for the feed description */
public static final String PROP_DESCRIPTION = "feed.description";
/** Property key for the feed copyright note */
public static final String PROP_COPYRIGHT = "feed.copyright";
/** Property key for the feed home url */
public static final String PROP_HOME = "feed.home";
/** Property key for the feed cover url */
public static final String PROP_COVER = "feed.cover";
/** Property key for the feed entry link template */
public static final String PROP_ENTRY = "feed.entry";
/** Property key for the feed entry rel=self link template */
public static final String PROP_SELF = "feed.self";
/** Property key for the feed rss media element flavor */
public static final String PROP_RSSFLAVORS = "feed.rssflavors";
/** Property key for the feed atom media element flavor */
public static final String PROP_ATOMFLAVORS = "feed.atomflavors";
/** Property key for the feed rss media element flavor */
public static final String PROP_RSSTAGS = "feed.rsstags";
/** Property key for the feed rss media type */
public static final String PROP_RSS_MEDIA_TYPE = "feed.rssmediatype";
/** Property key for the feed atom media element flavor */
public static final String PROP_ATOMTAGS = "feed.atomtags";
/** A default value for limit */
protected static final int DEFAULT_LIMIT = 100;
/** Unlimited */
protected static final int NO_LIMIT = Integer.MAX_VALUE;
/** A default value for offset */
protected static final int DEFAULT_OFFSET = 0;
/** The date parser format **/
protected static final String DATE_FORMAT = "dd.MM.yyyy HH:mm:ss";
/** The default feed encoding */
public static final String ENCODING = "UTF-8";
/** Default rss media type */
public static final String PROP_RSS_MEDIA_TYPE_DEFAULT = "*";
/** Link to the user interface */
private String linkTemplate = null;
/** Link to the user alternative interface */
private String linkSelf = null;
/** The feed homepage */
private String home = null;
/** Default format for rss feeds */
private List<MediaPackageElementFlavor> rssTrackFlavors = null;
/** The */
private List<String> rssMediaTypes = null;
/** Formats for atom feeds */
private Set<MediaPackageElementFlavor> atomTrackFlavors = null;
/** Tags used to mark rss tracks */
private Set<String> rssTags = null;
/** Tags used to mark atom tracks */
private Set<String> atomTags = null;
/** the feed uri */
private String uri = null;
/** the feed size */
private int size = DEFAULT_LIMIT;
/** The feed name */
private String name = null;
/** Url to the cover image */
private String cover = null;
/** Copyright notice */
private String copyright = null;
/** The feed description */
private String description = null;
/** The URL of the server for valid URIs in the feeds */
private String serverUrl = null;
/**
* Creates a new abstract feed generator.
* <p>
* <b>Note:</b> Subclasses using this constructor need to set required member variables prior to calling
* createFeed for the first time.
*/
protected AbstractFeedGenerator() {
atomTrackFlavors = new HashSet<MediaPackageElementFlavor>();
rssTrackFlavors = new ArrayList<MediaPackageElementFlavor>();
rssMediaTypes = new ArrayList<String>();
rssTags = new HashSet<String>();
atomTags = new HashSet<String>();
}
/**
* Creates a new abstract feed generator.
*
* @param uri
* the feed identifier
* @param feedHome
* the feed's home url
* @param rssFlavors
* the ordered list of flavors identifying the track to be included in rss feeds
* @param rssMediaTypes
* the ordered list of media types to include in rss feeds
* @param atomFlavors
* the flavors identifying tracks to be included in atom feeds
* @param entryLinkTemplate
* the link template
*/
public AbstractFeedGenerator(String uri, String feedHome, MediaPackageElementFlavor[] rssFlavors,
String[] rssMediaTypes, MediaPackageElementFlavor[] atomFlavors, String entryLinkTemplate) {
this();
this.uri = uri;
this.home = feedHome;
if (rssFlavors != null)
this.rssTrackFlavors.addAll(Arrays.asList(rssFlavors));
if (rssMediaTypes != null)
this.rssMediaTypes.addAll(Arrays.asList(rssMediaTypes));
this.linkTemplate = entryLinkTemplate;
if (atomFlavors != null)
this.atomTrackFlavors.addAll(Arrays.asList(atomFlavors));
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#initialize(java.util.Properties)
*/
@Override
public void initialize(Properties properties) {
serverUrl = (String) properties.get(MatterhornConstants.SERVER_URL_PROPERTY);
uri = generateFeedUri((String) properties.get(PROP_URI));
String sizeAsString = (String) properties.get(PROP_SIZE);
try {
if (StringUtils.isNotBlank(sizeAsString)) {
size = Integer.parseInt(sizeAsString);
if (size == 0)
size = Integer.MAX_VALUE;
}
} catch (NumberFormatException e) {
logger.warn("Unable to set the size of the feed to {}", sizeAsString);
}
name = (String) properties.get(PROP_NAME);
description = (String) properties.get(PROP_DESCRIPTION);
copyright = (String) properties.get(PROP_COPYRIGHT);
home = (String) properties.get(PROP_HOME);
// feed.cover can be unset if no branding is required
if (StringUtils.isBlank((String) properties.get(PROP_COVER))) {
cover = null;
} else {
cover = (String) properties.get(PROP_COVER);
}
linkTemplate = (String) properties.get(PROP_ENTRY);
if (properties.get(PROP_SELF) != null)
linkSelf = (String) properties.get(PROP_SELF);
String rssFlavors = (String) properties.get(PROP_RSSFLAVORS);
if (rssFlavors != null) {
StringTokenizer tok = new StringTokenizer(rssFlavors, " ,;");
while (tok.hasMoreTokens()) {
addRssTrackFlavor(MediaPackageElementFlavor.parseFlavor(tok.nextToken()));
}
}
String rssMediaTypes = (String) properties.get(PROP_RSS_MEDIA_TYPE);
if (rssFlavors == null) {
this.rssMediaTypes.add(PROP_RSS_MEDIA_TYPE_DEFAULT);
} else {
StringTokenizer tok = new StringTokenizer(rssMediaTypes, " ,;");
while (tok.hasMoreTokens()) {
this.rssMediaTypes.add(tok.nextToken());
}
}
String atomFlavors = (String) properties.get(PROP_ATOMFLAVORS);
if (atomFlavors != null) {
StringTokenizer tok = new StringTokenizer(atomFlavors, " ,;");
while (tok.hasMoreTokens()) {
addAtomTrackFlavor(MediaPackageElementFlavor.parseFlavor(tok.nextToken()));
}
}
String rssTags = (String) properties.get(PROP_RSSTAGS);
if (rssTags != null) {
for (String tag : rssTags.split("\\W")) {
addRSSTag(tag);
}
}
String atomTags = (String) properties.get(PROP_ATOMTAGS);
if (atomTags != null) {
for (String tag : atomTags.split("\\W")) {
addAtomTag(tag);
}
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#getIdentifier()
*/
public String getIdentifier() {
return uri;
}
/**
* Sets the feed name.
*
* @param name
* the feed name
*/
public void setName(String name) {
this.name = name;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#getName()
*/
public String getName() {
return name;
}
/**
* Sets the feed description.
*
* @param description
* the feed description
*/
public void setDescription(String description) {
this.description = description;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#getDescription()
*/
public String getDescription() {
return description;
}
/**
* Sets the copyright notice.
*
* @param copyright
* the copyright notice
*/
public void setCopyright(String copyright) {
this.copyright = copyright;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#getCopyright()
*/
@Override
public String getCopyright() {
return copyright;
}
/**
* Sets the url to the cover url.
*
* @param cover
* the cover url
*/
public void setCover(String cover) {
this.cover = cover;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.feed.api.FeedGenerator#getCover(Organization)
*/
@Override
public String getCover(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(cover, feedURL, engageUIURL, serverUrl);
}
/**
* Returns the feed's base URI.
*
* @return the feed uri
*/
protected String getURI() {
return uri;
}
/**
* Returns the default size of the feed.
*
* @return the size
*/
protected int getSize() {
return size;
}
/**
* Returns the link to the homepage.
*
* @param organization
* the organization
* @return the homepage
*/
protected String getHome(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(home, feedURL, engageUIURL, serverUrl);
}
/**
* Returns the template used to render link to feed items.
*
* @param organization
* the organization
* @return the link template
*/
public String getLinkTemplate(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(linkTemplate, feedURL, engageUIURL, serverUrl);
}
/**
* Returns the link to the feed itself.
*
* @param organization
* the organization
* @return the link to the feed
*/
public String getLinkToSelf(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(linkSelf, feedURL, engageUIURL, serverUrl);
}
/**
* Returns the feed uri
*
* @param feedId
* @return
*/
protected String generateFeedUri(String feedId) {
return ensureUrl(feedId, serverUrl);
}
/**
* Loads and returns the feed data.
*
* @param type
* the requested feed type
* @param query
* the query parameter
* @param limit
* the number of entries
* @param offset
* the starting entry
* @return the feed data
*/
protected abstract SearchResult loadFeedData(Feed.Type type, String[] query, int limit, int offset);
/**
* Creates a search query that is taking into account the flavors and tags as configured in the feed definition as
* well as limit and offset passed into this method.
*
* @param type
* the requested feed type
* @param limit
* the number of entries
* @param offset
* the starting entry
* @return the base query
*/
protected SearchQuery createBaseQuery(Feed.Type type, int limit, int offset) {
SearchQuery searchQuery = new SearchQuery();
searchQuery.withLimit(limit);
searchQuery.withOffset(offset);
searchQuery.withSort(SearchQuery.Sort.DATE_CREATED);
switch (type) {
case Atom:
if (atomTags != null && atomTags.size() > 0)
searchQuery.withElementTags(atomTags.toArray(new String[atomTags.size()]));
break;
case RSS:
if (rssTags != null && rssTags.size() > 0)
searchQuery.withElementTags(rssTags.toArray(new String[rssTags.size()]));
break;
default:
throw new IllegalStateException("Unknown feed type '" + type + "' is not supported");
}
return searchQuery;
}
/**
* Creates a new feed.
*
* @param type
* the feed type
* @param uri
* the feed identifier
* @param title
* the feed title
* @param description
* the feed description
* @param link
* the link to the feed homepage
* @return the new feed
*/
protected Feed createFeed(Feed.Type type, String uri, Content title, Content description, String link) {
return new FeedImpl(type, uri, title, description, link);
}
/**
* {@inheritDoc}
*/
public final Feed createFeed(Feed.Type type, String[] query, int size, Organization organization) {
logger.debug("Started to create {} feed", type);
SearchResult result = null;
if (type == null)
throw new IllegalArgumentException("Feed type must not be null");
if (size <= 0) {
logger.trace("Using the feed's configured size of {}", this.size);
size = this.size;
}
// Check if the feed generator is correctly set up
if (uri == null)
throw new IllegalStateException("Feed uri (feed.uri) must be configured");
if (name == null)
throw new IllegalStateException("Feed name (feed.name) must be configured");
if (getHome(organization) == null)
throw new IllegalStateException("Feed url (feed.home) must be configured");
if (getLinkTemplate(organization) == null)
throw new IllegalStateException("Feed link template (feed.entry) must be configured");
// Have the concrete implementation load the feed data
result = loadFeedData(type, query, size, DEFAULT_OFFSET);
if (result == null) {
logger.debug("Cannot retrieve solr result for feed '" + type.toString() + "' with query '" + query + "'.");
return null;
}
// Create the feed
Feed f = createFeed(type, getIdentifier(), new ContentImpl(getName()), new ContentImpl(getDescription()),
getFeedLink(organization));
f.setEncoding(ENCODING);
// Set iTunes tags
ITunesFeedExtension iTunesFeed = new ITunesFeedExtension();
f.addModule(iTunesFeed);
if (cover != null)
f.setImage(new ImageImpl(cover, "Feed Image"));
// Check if a default format has been specified
// TODO: Parse flavor and set member variable rssTrackFlavor
// String rssFlavor = query.length > 1 ? query[query.length - 1] : null;
// Iterate over the feed data and create the entries
int itemCount = 0;
for (SearchResultItem resultItem : result.getItems()) {
try {
if (resultItem.getType().equals(SearchResultItemType.Series))
addSeries(f, query, resultItem, organization);
else
addEpisode(f, query, resultItem, organization);
} catch (Throwable t) {
logger.error(
"Error creating entry with id " + resultItem.getId() + " for feed " + this + ": " + t.getMessage(), t);
}
itemCount++;
if (itemCount >= size)
break;
}
return f;
}
/**
* Adds series information to the feed.
*
* @param feed
* the feed
* @param query
* the query that results in the feed
* @param resultItem
* the series item
* @param organization
* the organization
* @return the feed
*/
protected Feed addSeries(Feed feed, String[] query, SearchResultItem resultItem, Organization organization) {
Date d = resultItem.getDcCreated();
// find iTunes module
ITunesFeedExtension iTunesFeed = null;
for (FeedExtension extension : feed.getModules()) {
if (extension instanceof ITunesFeedExtension) {
iTunesFeed = (ITunesFeedExtension) extension;
break;
}
}
if (!StringUtils.isEmpty(resultItem.getDcTitle()))
feed.setTitle(resultItem.getDcTitle());
if (!StringUtils.isEmpty(resultItem.getDcDescription())) {
feed.setDescription(resultItem.getDcDescription());
if (iTunesFeed != null)
iTunesFeed.setSummary(resultItem.getDcDescription());
}
if (!StringUtils.isEmpty(resultItem.getDcCreator())) {
PersonImpl personImpl = new PersonImpl(resultItem.getDcCreator());
feed.addAuthor(personImpl);
if (iTunesFeed != null) {
iTunesFeed.setAuthor(personImpl.getName());
iTunesFeed.setOwnerName(personImpl.getName());
}
}
if (!StringUtils.isEmpty(resultItem.getDcContributor()))
feed.addContributor(new PersonImpl(resultItem.getDcContributor()));
if (!StringUtils.isEmpty(resultItem.getDcAccessRights()))
feed.setCopyright(resultItem.getDcAccessRights());
if (!StringUtils.isEmpty(resultItem.getDcLanguage()))
feed.setLanguage(resultItem.getDcLanguage());
feed.setUri(resultItem.getId());
feed.addLink(new LinkImpl(getLinkForEntry(feed, resultItem, organization)));
if (d != null)
feed.setPublishedDate(d);
// Set the cover image
String coverUrl = null;
if (!StringUtils.isEmpty(resultItem.getCover())) {
coverUrl = resultItem.getCover();
feed.setImage(new ImageImpl(coverUrl, resultItem.getDcTitle()));
try {
if (iTunesFeed != null)
iTunesFeed.setImage(new URL(coverUrl));
} catch (MalformedURLException e) {
logger.error("Error creating cover URL from " + coverUrl);
}
}
return feed;
}
/**
* Adds episode information to the feed.
*
* @param feed
* the feed
* @param query
* the query that results in the feed
* @param resultItem
* the episodes item
* @param organization
* the organization
* @return the feed
*/
protected Feed addEpisode(Feed feed, String[] query, SearchResultItem resultItem, Organization organization) {
String link = getLinkForEntry(feed, resultItem, organization);
String title = resultItem.getDcTitle();
// Get the media enclosures
List<MediaPackageElement> enclosures = getEnclosures(feed, resultItem);
if (enclosures.size() == 0) {
logger.debug("No media formats found for feed entry {}", title);
return feed;
}
String entryUri = null;
// For RSS feeds, create multiple entries (one per enclosure). For Atom, add all enclosures to the same item
switch (feed.getType()) {
case RSS:
entryUri = resultItem.getId();
for (MediaPackageElement e : enclosures) {
List<MediaPackageElement> enclosure = new ArrayList<MediaPackageElement>(1);
enclosure.add(e);
FeedEntry entry = createEntry(feed, title, link, entryUri);
entry = populateFeedEntry(entry, resultItem, enclosure);
entry.setUri(entry.getUri() + "/" + e.getIdentifier());
feed.addEntry(entry);
}
break;
case Atom:
entryUri = generateEntryUri(resultItem.getId());
FeedEntry entry = createEntry(feed, title, link, entryUri);
entry = populateFeedEntry(entry, resultItem, enclosures);
if (getLinkSelf(organization) != null) {
LinkImpl self = new LinkImpl(getSelfLinkForEntry(feed, resultItem, organization));
self.setRel("self");
entry.addLink(self);
}
feed.addEntry(entry);
if (feed.getUpdatedDate() == null)
feed.setUpdatedDate(entry.getUpdatedDate());
else if (entry.getUpdatedDate().before(feed.getUpdatedDate()))
feed.setUpdatedDate(entry.getUpdatedDate());
break;
default:
throw new IllegalStateException("Unsupported feed type " + feed.getType());
}
return feed;
}
protected String generateEntryUri(String id) {
return serverUrl + "/entry-uri/" + id;
}
/**
* Populates the feed entry with metadata and the enclosures.
*
* @param entry
* the entry to enrich
* @param metadata
* the metadata
* @param enclosures
* the media enclosures
* @return the enriched item
*/
private FeedEntry populateFeedEntry(FeedEntry entry, SearchResultItem metadata, List<MediaPackageElement> enclosures) {
Date d = metadata.getDcCreated();
Date updatedDate = metadata.getModified();
String title = metadata.getDcTitle();
// Configure the iTunes extension
ITunesFeedEntryExtension iTunesEntry = new ITunesFeedEntryExtension();
iTunesEntry.setDuration(metadata.getDcExtent());
iTunesEntry.setBlocked(false);
iTunesEntry.setExplicit(false);
if (StringUtils.isNotBlank(metadata.getDcCreator()))
iTunesEntry.setAuthor(metadata.getDcCreator());
// TODO: Add iTunes keywords and subtitles
// iTunesEntry.setKeywords(keywords);
// iTunesEntry.setSubtitle(subtitle);
// Configure the DC extension
DublinCoreExtension dcExtension = new DublinCoreExtension();
dcExtension.setTitle(title);
dcExtension.setIdentifier(metadata.getId());
// Set contributor
if (!StringUtils.isEmpty(metadata.getDcContributor())) {
for (String contributor : metadata.getDcContributor().split(";;")) {
entry.addContributor(new PersonImpl(contributor));
dcExtension.addContributor(contributor);
}
}
// Set creator
if (!StringUtils.isEmpty(metadata.getDcCreator())) {
for (String creator : metadata.getDcCreator().split(";;")) {
if (iTunesEntry.getAuthor() == null)
iTunesEntry.setAuthor(creator);
entry.addAuthor(new PersonImpl(creator));
dcExtension.addCreator(creator);
}
}
// Set publisher
if (!StringUtils.isEmpty(metadata.getDcPublisher())) {
dcExtension.addPublisher(metadata.getDcPublisher());
}
// Set rights
if (!StringUtils.isEmpty(metadata.getDcAccessRights())) {
dcExtension.setRights(metadata.getDcAccessRights());
}
// Set description
if (!StringUtils.isEmpty(metadata.getDcDescription())) {
String summary = metadata.getDcDescription();
entry.setDescription(new ContentImpl(summary));
iTunesEntry.setSummary(summary);
dcExtension.setDescription(summary);
}
// Set the language
if (!StringUtils.isEmpty(metadata.getDcLanguage())) {
dcExtension.setLanguage(metadata.getDcLanguage());
}
// Set the publication date
if (d != null) {
entry.setPublishedDate(d);
dcExtension.setDate(d);
} else if (metadata.getModified() != null) {
entry.setPublishedDate(metadata.getModified());
dcExtension.setDate(metadata.getModified());
}
// Set the updated date
if (updatedDate == null)
updatedDate = d;
entry.setUpdatedDate(updatedDate);
// TODO: Finish dc support
// Set format
// if (!StringUtils.isEmpty(resultItem.getMediaType())) {
// dcExtension.setFormat(resultItem.getMediaType());
// }
// dcEntry.setCoverage(arg0);
// dcEntry.setRelation(arg0);
// dcEntry.setSource(arg0);
// dcEntry.setSubject(arg0);
// Set the cover image
String coverUrl = null;
if (!StringUtils.isEmpty(metadata.getCover())) {
coverUrl = metadata.getCover();
setImage(entry, coverUrl);
}
entry.addExtension(iTunesEntry);
entry.addExtension(dcExtension);
// Add the enclosures
for (MediaPackageElement element : enclosures) {
String trackMimeType = element.getMimeType().toString();
long trackLength = element.getSize();
if (trackLength <= 0 && element instanceof Track) {
// filesize unset so estimate from duration and bitrate
trackLength = 0;
if (((TrackImpl) element).hasVideo()) {
List<VideoStream> video = ((TrackImpl) element).getVideo();
if (video.get(0).getBitRate() != null) {
trackLength += metadata.getDcExtent() / 1000 * video.get(0).getBitRate() / 8;
}
}
if (((TrackImpl) element).hasAudio()) {
List<AudioStream> audio = ((TrackImpl) element).getAudio();
if (audio.get(0).getBitRate() != null) {
trackLength += metadata.getDcExtent() / 1000 * audio.get(0).getBitRate() / 8;
}
}
}
// if no bitrate data default to value of duration which is probably within an
// order of magnitude correct
if (trackLength <= 0) {
trackLength = metadata.getDcExtent();
}
String trackFlavor = element.getFlavor().toString();
String trackUrl = null;
try {
trackUrl = element.getURI().toURL().toExternalForm();
} catch (MalformedURLException e) {
// Can't happen
}
Enclosure enclosure = new EnclosureImpl(trackUrl, trackMimeType, trackFlavor, trackLength);
entry.addEnclosure(enclosure);
}
return entry;
}
/**
* Creates a new feed entry that can be added to the feed.
*
* @param feed
* the feed that is being created
* @param title
* the entry title
* @param link
* link to the orginal resource
* @param uri
* the entry uri
* @return the feed
*/
protected FeedEntry createEntry(Feed feed, String title, String link, String uri) {
return createEntry(feed, title, null, link, uri);
}
/**
* Creates a new feed entry that can be added to the feed.
*
* @param feed
* the feed that is being created
* @param title
* the entry title
* @param description
* the entry description
* @param link
* link to the orginal resource
* @param uri
* the entry uri
* @return the feed
*/
protected FeedEntry createEntry(Feed feed, String title, String description, String link, String uri) {
if (feed == null)
throw new IllegalStateException("Feed must be created prior to creating feed entries");
FeedEntryImpl entry = new FeedEntryImpl(feed, title, description, new LinkImpl(link), uri);
return entry;
}
/**
* Adds the image as a content element to the feed entry.
*
* @param entry
* the feed entry
* @param imageUrl
* the image url
* @return the image
*/
protected Content setImage(FeedEntry entry, String imageUrl) {
StringBuffer buf = new StringBuffer("<div xmlns=\"http://www.w3.org/1999/xhtml\">");
buf.append("<img src=\"");
buf.append(imageUrl);
buf.append("\" />");
buf.append("</div>");
Content image = new ContentImpl(buf.toString(), "application/xhtml+xml", Mode.Xml);
entry.addContent(image);
return image;
}
/**
* Sets the rss media types.
*
* @param mediaTypes
* The ordered array of media types to choose for RSS feeds.
*/
protected void setRssMediatypes(String[] mediaTypes) {
if (mediaTypes == null || mediaTypes.length == 0) {
throw new IllegalArgumentException("mediaTypes are required for an rss feed");
}
this.rssMediaTypes.clear();
this.rssMediaTypes.addAll(Arrays.asList(mediaTypes));
}
/**
* Returns an ordered list of media types for the RSS feed. The feed will select the first media type where a track is
* available.
*
* @return The rss media types
*/
protected List<String> getRssMediaTypes() {
return rssMediaTypes;
}
/**
* Adds the flavor to the set of flavors of tracks that are to be included in rss feeds.
*
* @param flavor
* the flavor to add
*/
protected void addRssTrackFlavor(MediaPackageElementFlavor flavor) {
rssTrackFlavors.add(flavor);
}
/**
* Removes the flavor from the set of flavors of tracks that are to be included in rss feeds.
*
* @param flavor
* the flavor to add
*/
protected void removeRssTrackFlavor(MediaPackageElementFlavor flavor) {
rssTrackFlavors.remove(flavor);
}
/**
* Returns the ordered list of flavors of the tracks to be included in rss feeds. If the first flavor specified in
* this list is not available, we try to use the next flavor, until a track is found or no more flavors are available.
*
* @return the flavors for rss feed tracks
*/
protected List<MediaPackageElementFlavor> getRssTrackFlavors() {
return rssTrackFlavors;
}
/**
* Adds the flavor to the set of flavors of tracks that are to be included in atom feeds.
*
* @param flavor
* the flavor to add
*/
protected void addAtomTrackFlavor(MediaPackageElementFlavor flavor) {
atomTrackFlavors.add(flavor);
}
/**
* Removes the flavor from the set of flavors of tracks that are to be included in atom feeds.
*
* @param flavor
* the flavor to add
*/
protected void removeAtomTrackFlavor(MediaPackageElementFlavor flavor) {
atomTrackFlavors.remove(flavor);
}
/**
* Returns the flavors of the tracks to be included in atom feeds.
*
* @return the flavors for atom feed tracks
*/
protected Set<MediaPackageElementFlavor> getAtomTrackFlavors() {
return atomTrackFlavors;
}
/**
* Adds the tag to the set of tags that identify the tracks that are to be included in atom feeds.
*
* @param tag
* the tag to add
*/
protected void addAtomTag(String tag) {
atomTags.add(tag);
}
/**
* Removes the tag from the set of tags that identify the tracks that are to be included in atom feeds.
*
* @param tag
* the tag to add
*/
protected void removeAtomTag(String tag) {
atomTags.remove(tag);
}
/**
* Returns the tags of the tracks to be included in atom feeds.
*
* @return the tags for atom feed tracks
*/
protected Set<String> getAtomTags() {
return atomTags;
}
/**
* Adds the tag to the set of tags that identify the tracks that are to be included in rss feeds.
*
* @param tag
* the tag to add
*/
protected void addRSSTag(String tag) {
rssTags.add(tag);
}
/**
* Removes the tag from the set of tags that identify the tracks that are to be included in rss feeds.
*
* @param tag
* the tag to add
*/
protected void removeRSSTag(String tag) {
rssTags.remove(tag);
}
/**
* Returns the tags of the tracks to be included in rss feeds.
*
* @return the tags for rss feed tracks
*/
protected Set<String> getRSSTags() {
return rssTags;
}
/**
* Returns the identifier of those tracks that are to be included as enclosures in the feed. Note that for a feed type
* of {@link Feed.Type#RSS}, the list must exactly contain one single entry.
* <p>
* This default implementation will include the track identified by the flavor as specified in the constructor for rss
* feeds and every distribution track for atom feeds.
*
* @param feed
* the feed
* @param resultItem
* the result item
* @return the set of identifier
*/
protected List<MediaPackageElement> getEnclosures(Feed feed, SearchResultItem resultItem) {
MediaPackage mediaPackage = resultItem.getMediaPackage();
List<MediaPackageElement> candidateElements = new ArrayList<MediaPackageElement>();
List<MediaPackageElementFlavor> flavors = new ArrayList<MediaPackageElementFlavor>();
Set<String> tags = new HashSet<String>();
switch (feed.getType()) {
case Atom:
flavors.addAll(atomTrackFlavors);
tags.addAll(atomTags);
break;
default:
flavors.addAll(rssTrackFlavors);
tags.addAll(rssTags);
break;
}
// Collect track id's by flavor
if (flavors.size() > 0) {
for (MediaPackageElementFlavor flavor : flavors) {
MediaPackageElement[] elements = mediaPackage.getElementsByFlavor(flavor);
for (MediaPackageElement element : elements) {
if (element.containsTag(tags)) {
candidateElements.add(element);
}
}
}
}
if (candidateElements.size() == 0) {
logger.debug("No distributed media found for feed entry");
}
return candidateElements;
}
/**
* Sets the url to the feed's homepage.
*
* @param url
* the homepage
*/
public void setFeedLink(String url) {
this.home = url;
}
/**
* Returns the url to the feed's homepage.
*
* @param organization
* the organization
* @return the feed home
*/
public String getFeedLink(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(home, feedURL, engageUIURL, serverUrl);
}
/**
* Sets the entry's base url that will be used to form the episode link in the feeds. If the url contains a
* placeholder in the form <code>{0}</code>, it will be replaced by the episode id.
*
* @param url
* the url
*/
public void setLinkTemplate(String url) {
linkTemplate = url;
}
/**
* Sets the entry's base url that will be used to form the alternate episode link in the feeds. If the url contains a
* placeholder in the form <code>{0}</code>, it will be replaced by the episode id.
*
* @param url
* the url
*/
public void setLinkSelf(String url) {
linkSelf = url;
}
/**
* Returns the self link template to the default user interface.
*
* @return the link to the ui
*/
public String getLinkSelf(Organization organization) {
String feedURL = organization.getProperties().get(PROP_ORG_FEED_URL);
String engageUIURL = organization.getProperties().get(PROP_ORG_ENGAGE_UI_URL);
return ensureUrl(linkSelf, feedURL, engageUIURL, serverUrl);
}
/**
* Generates a link for the current feed entry by using the entry identifier and the result of
* getLinkTemplate() to create the url. Overwrite this method to provide your own way of generating links to
* feed entries.
*
* @param feed
* the feed
* @param solrResultItem
* solr search result for this feed entry
* @param organization
* the organization
* @return the link to the ui
*/
protected String getLinkForEntry(Feed feed, SearchResultItem solrResultItem, Organization organization) {
return MessageFormat.format(getLinkTemplate(organization), solrResultItem.getId());
}
/**
* Generates a link for the current feed entry by using the entry identifier and the result of #getLinkSelf()
* to create the url. Overwrite this method to provide your own way of generating links to feed entries.
*
* @param feed
* the feed
* @param solrResultItem
* solr search result for this feed entry
* @param organization
* the organization
* @return the link to the ui
*/
protected String getSelfLinkForEntry(Feed feed, SearchResultItem solrResultItem, Organization organization) {
return MessageFormat.format(getLinkSelf(organization), solrResultItem.getId());
}
/**
* Ensures that this string is an absolute URL. If not, prepend the local serverUrl to the string.
*
* @param string
* The absolute or relative URL
* @param baseUrl
* The base URL to prepend
* @return An absolute URL
*/
protected String ensureUrl(String string, String... baseUrl) {
String pathOrUrl = StringUtils.trimToEmpty(string);
try {
new URL(pathOrUrl);
return pathOrUrl;
} catch (Exception e) {
for (String url : baseUrl) {
if (StringUtils.isNotBlank(url)) {
return UrlSupport.concat(url, pathOrUrl);
}
}
throw new IllegalArgumentException("All potential base urls were blank");
}
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return uri.hashCode();
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof FeedGenerator))
return false;
FeedGenerator generator = (FeedGenerator) o;
return getIdentifier().equals(generator.getIdentifier());
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
if (this.getName() != null)
return getName();
return super.toString();
}
}