org.jboss.seam.wiki.core.ui.FeedServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.seam.wiki.core.ui.FeedServlet.java

Source

/*
 * 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());
    }
}