/*
* JBoss, Home of Professional Open Source
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.seam.wiki.core.ui;
import com.sun.syndication.feed.synd.*;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.FeedException;
import org.jboss.seam.Component;
import org.jboss.seam.servlet.ContextualHttpServletRequest;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.international.Messages;
import org.jboss.seam.wiki.core.feeds.FeedDAO;
import org.jboss.seam.wiki.core.model.*;
import org.jboss.seam.wiki.core.action.Authenticator;
import org.jboss.seam.wiki.core.action.prefs.WikiPreferences;
import org.jboss.seam.wiki.core.dao.WikiNodeDAO;
import org.jboss.seam.wiki.core.dao.WikiNodeFactory;
import org.jboss.seam.wiki.util.WikiUtil;
import org.jboss.seam.wiki.util.Hash;
import org.jboss.seam.wiki.preferences.Preferences;
import org.jboss.seam.wiki.connectors.feed.FeedAggregateCache;
import org.jboss.seam.wiki.connectors.feed.FeedEntryDTO;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
/**
* Serves syndicated feeds, one feed for each directory that has a feed.
* <p>
* This servlet uses either the currently logged in user (session) or
* basic HTTP authorization if there is no user logged in or if the feed
* requires a higher access level than currently available. Feed entries are also
* read-access filtered. Optionally, requests can enable/disable comments on the feed
* or filter by tag. It's up to the actual <tt>WikiFeedEntry</tt> instance how these
* filters are applied.
* </p>
*
* @author Christian Bauer
*/
public class FeedServlet extends HttpServlet {
private static final Log log = LogFactory.getLog(FeedServlet.class);
public static enum Comments {
include, exclude, only
}
// Possible feed types
public enum SyndFeedType {
ATOM("/atom.seam", "atom_1.0", "application/atom+xml");
// TODO: I don't think we'll ever do that: ,RSS2("/rss.seam", "rss_2.0", "application/rss+xml");
SyndFeedType(String pathInfo, String feedType, String contentType) {
this.pathInfo = pathInfo;
this.feedType = feedType;
this.contentType = contentType;
}
String pathInfo;
String feedType;
String contentType;
}
// Supported feed types
private Map<String, SyndFeedType> feedTypes = new HashMap<String,SyndFeedType>() {{
put(SyndFeedType.ATOM.pathInfo, SyndFeedType.ATOM);
}};
// Allow unit testing
public FeedServlet() {}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
new ContextualHttpServletRequest(request) {
@Override
public void process() throws Exception {
doWork(request, response);
}
}.run();
}
// TODO: All data access in this method runs with auto-commit mode, see http://jira.jboss.com/jira/browse/JBSEAM-957
protected void doWork(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String feedIdParam = request.getParameter("feedId");
String areaNameParam = request.getParameter("areaName");
String nodeNameParam = request.getParameter("nodeName");
String aggregateParam = request.getParameter("aggregate");
log.debug(">>> feed request id: '" + feedIdParam + "' area name: '" + areaNameParam + "' node name: '" + nodeNameParam + "'");
Contexts.getSessionContext().set("LAST_ACCESS_ACTION", "Feed: " +feedIdParam + " area: '" + areaNameParam + "' node: '" + nodeNameParam + "'");
// Feed type
String pathInfo = request.getPathInfo();
log.debug("requested feed type: " + pathInfo);
if (!feedTypes.containsKey(pathInfo)) {
log.debug("can not render this feed type, returning BAD REQUEST");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsupported feed type " + pathInfo);
return;
}
SyndFeedType syndFeedType = feedTypes.get(pathInfo);
// Comments
String commentsParam = request.getParameter("comments");
Comments comments = Comments.include;
if (commentsParam != null && commentsParam.length() >0) {
try {
comments = Comments.valueOf(commentsParam);
} catch (IllegalArgumentException ex) {
log.info("invalid comments request parameter: " + commentsParam);
}
}
log.debug("feed rendering handles comments: " + comments);
// Tag
String tagParam = request.getParameter("tag");
String tag = null;
if (tagParam != null && tagParam.length() >0) {
log.debug("feed rendering restricts on tag: " + tagParam);
tag = tagParam;
}
Feed feed = resolveFeed(aggregateParam, feedIdParam, areaNameParam, nodeNameParam);
if (feed == null) {
log.debug("feed not found, returning NOT FOUND");
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Feed");
return;
}
log.debug("checking permissions of " + feed);
// Authenticate and authorize, first with current user (session) then with basic HTTP authentication
Integer currentAccessLevel = (Integer)Component.getInstance("currentAccessLevel");
if (feed.getReadAccessLevel() > currentAccessLevel) {
boolean loggedIn = ((Authenticator)Component.getInstance(Authenticator.class)).authenticateBasicHttp(request);
currentAccessLevel = (Integer)Component.getInstance("currentAccessLevel");
if (!loggedIn || feed.getReadAccessLevel() > currentAccessLevel) {
log.debug("requiring authentication, feed has higher access level than current");
response.setHeader("WWW-Authenticate", "Basic realm=\"" + feed.getTitle().replace("\"", "'") + "\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
Date lastFeedEntryDate = null;
if (feed.getId() != null) {
// Ask the database what the latest feed entry is for that feed, then use its updated timestamp hash
FeedDAO feedDAO = (FeedDAO)Component.getInstance(FeedDAO.class);
List<FeedEntry> result = feedDAO.findLastFeedEntries(feed.getId(), 1);
if (result.size() > 0) {
lastFeedEntryDate = result.get(0).getUpdatedDate();
}
} else {
// Get the first (latest) entry of the aggregated feed and use its published timestamp hash (ignoring updates!)
// There is a wrinkle hidden here: What if a feed entry is updated? Then the published timestamp should also
// be different because the "first latest" feed entry in the list is sorted by both published and updated
// timestamps. So even though we only use published timestamp hash as an ETag, this timestamp also changes
// when a feed entry is updated because the collection order changes as well.
if (feed.getFeedEntries().size() > 0) {
lastFeedEntryDate = feed.getFeedEntries().iterator().next().getPublishedDate();
}
}
if (lastFeedEntryDate != null) {
String etag = calculateEtag(lastFeedEntryDate);
log.debug("setting etag header: " + etag);
response.setHeader("ETag", etag);
String previousToken = request.getHeader("If-None-Match");
if (previousToken != null && previousToken.equals(etag)) {
log.debug("found matching etag in request header, returning 304 Not Modified");
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
}
// TODO: Refactor this parameter mess a little
log.debug("finally rendering feed");
SyndFeed syndFeed =
createSyndFeed(
request.getRequestURL().toString(),
syndFeedType,
feed,
currentAccessLevel,
tag,
comments,
aggregateParam
);
// If we have an entry on this feed, take the last entry's update timestamp and use it as
// the published timestamp of the feed. The Rome library does not have a setUpdatedDate()
// method and abuses the published date to write <updated> into the Atom <feed> element.
if (lastFeedEntryDate != null) {
syndFeed.setPublishedDate(lastFeedEntryDate);
}
// Write feed to output
response.setContentType(syndFeedType.contentType);
response.setCharacterEncoding("UTF-8");
SyndFeedOutput output = new SyndFeedOutput();
try {
output.output(syndFeed, response.getWriter());
} catch (FeedException ex) {
throw new ServletException(ex);
}
response.getWriter().flush();
log.debug("<<< feed rendering complete");
}
public Feed resolveFeed(String aggregateParam, String feedIdParam, String areaNameParam, String nodeNameParam) {
Feed feed;
// Find the feed, depending on variations of request parameters
if (aggregateParam != null && aggregateParam.length() > 0) {
feed = resolveFeedWithAggregateId(aggregateParam);
} else if (feedIdParam != null && feedIdParam.length() >0) {
feed = resolveFeedWithFeedId(feedIdParam);
} else if (areaNameParam != null && areaNameParam.length() > 0) {
feed = resolveFeedWithAreaNameAndNodeName(areaNameParam, nodeNameParam);
} else {
log.debug("no aggregate id, no feed id, no area name requested, getting wikiRoot feed");
WikiNodeFactory factory = (WikiNodeFactory)Component.getInstance(WikiNodeFactory.class);
feed = factory.loadWikiRoot().getFeed();
}
return feed;
}
public Feed resolveFeedWithAggregateId(String aggregateId) {
Feed feed = null;
log.debug("trying to retrieve aggregated feed from cache: " + aggregateId);
FeedAggregateCache aggregateCache = (FeedAggregateCache)Component.getInstance(FeedAggregateCache.class);
List<FeedEntryDTO> result = aggregateCache.get(aggregateId);
if (result != null) {
feed = new Feed();
feed.setAuthor(Messages.instance().get("lacewiki.msg.AutomaticallyGeneratedFeed"));
feed.setTitle(Messages.instance().get("lacewiki.msg.AutomaticallyGeneratedFeed") + ": " + aggregateId);
feed.setPublishedDate(new Date());
// We are lying here, we don't really have an alternate representation link for this resource
feed.setLink( Preferences.instance().get(WikiPreferences.class).getBaseUrl() );
for (FeedEntryDTO feedEntryDTO : result) {
feed.getFeedEntries().add(feedEntryDTO.getFeedEntry());
}
}
return feed;
}
public Feed resolveFeedWithFeedId(String feedId) {
Feed feed = null;
try {
log.debug("trying to retrieve feed for id: " + feedId);
Long feedIdentifier = Long.valueOf(feedId);
FeedDAO feedDAO = (FeedDAO)Component.getInstance(FeedDAO.class);
feed = feedDAO.findFeed(feedIdentifier);
} catch (NumberFormatException ex) {
log.debug("feed identifier couldn't be converted to java.lang.Long");
}
return feed;
}
public Feed resolveFeedWithAreaNameAndNodeName(String areaName, String nodeName) {
Feed feed = null;
if (!areaName.matches("^[A-Z0-9]+.*")) return feed;
log.debug("trying to retrieve area: " + areaName);
WikiNodeDAO nodeDAO = (WikiNodeDAO)Component.getInstance(WikiNodeDAO.class);
WikiDirectory area = nodeDAO.findAreaUnrestricted(areaName);
if (area != null && (nodeName == null || !nodeName.matches("^[A-Z0-9]+.*")) && area.getFeed() != null) {
log.debug("using feed of area, no node requested: " + area);
feed = area.getFeed();
} else if (area != null && nodeName != null && nodeName.matches("^[A-Z0-9]+.*")) {
log.debug("trying to retrieve node: " + nodeName);
WikiDirectory nodeDir = nodeDAO.findWikiDirectoryInAreaUnrestricted(area.getAreaNumber(), nodeName);
if (nodeDir != null && nodeDir.getFeed() != null) {
log.debug("using feed of node: " + nodeDir);
feed = nodeDir.getFeed();
} else {
log.debug("node not found or node has no feed");
}
} else {
log.debug("area not found or area has no feed");
}
return feed;
}
public SyndFeed createSyndFeed(String baseURI, SyndFeedType syndFeedType, Feed feed, Integer currentAccessLevel) {
return createSyndFeed(baseURI, syndFeedType, feed, currentAccessLevel, null, Comments.include, null);
}
public SyndFeed createSyndFeed(String baseURI,
SyndFeedType syndFeedType,
Feed feed,
Integer currentAccessLevel,
String tag,
Comments comments,
String aggregateParam) {
WikiPreferences prefs = Preferences.instance().get(WikiPreferences.class);
// Create feed
SyndFeed syndFeed = new SyndFeedImpl();
String feedUri =
feed.getId() != null
? "?feedId="+feed.getId()
: "?aggregate="+WikiUtil.encodeURL(aggregateParam);
syndFeed.setUri(baseURI + feedUri);
syndFeed.setFeedType(syndFeedType.feedType);
syndFeed.setTitle(prefs.getFeedTitlePrefix() + feed.getTitle());
if (tag != null) {
syndFeed.setTitle(
syndFeed.getTitle() + " - " + Messages.instance().get("lacewiki.label.tagDisplay.Tag") + " '" + tag + "'"
);
}
syndFeed.setLink(feed.getLink());
syndFeed.setAuthor(feed.getAuthor());
if (feed.getDescription() != null && feed.getDescription().length() >0)
syndFeed.setDescription(feed.getDescription());
// Setting the date on which the local feed was stored in the database, might be overwritten later
syndFeed.setPublishedDate(feed.getPublishedDate());
// Create feed entries
List<SyndEntry> syndEntries = new ArrayList<SyndEntry>();
SortedSet<FeedEntry> entries = feed.getFeedEntries();
for (FeedEntry entry : entries) {
if (entry.getReadAccessLevel() > currentAccessLevel) continue;
if (tag != null && !entry.isTagged(tag)) continue;
if (comments.equals(Comments.exclude) && entry.isInstance(WikiCommentFeedEntry.class)) continue;
if (comments.equals(Comments.only) && !entry.isInstance(WikiCommentFeedEntry.class)) continue;
SyndEntry syndEntry;
syndEntry = new SyndEntryImpl();
syndEntry.setTitle(entry.getTitlePrefix() + entry.getTitle() + entry.getTitleSuffix());
syndEntry.setLink(entry.getLink());
syndEntry.setUri(entry.getLink());
syndEntry.setAuthor(entry.getAuthor());
syndEntry.setPublishedDate(entry.getPublishedDate());
syndEntry.setUpdatedDate(entry.getUpdatedDate());
SyndContent description;
description = new SyndContentImpl();
description.setType(entry.getDescriptionType());
description.setValue(WikiUtil.removeMacros(entry.getDescriptionValue()));
syndEntry.setDescription(description);
syndEntries.add(syndEntry);
}
syndFeed.setEntries(syndEntries);
return syndFeed;
}
private String calculateEtag(Date date) {
Hash hash = new Hash();
return hash.hash(date.toString());
}
}