Package ch.entwine.weblounge.dispatcher.impl.handler

Source Code of ch.entwine.weblounge.dispatcher.impl.handler.FeedRequestHandlerImpl

/*
*  Weblounge: Web Content Management System
*  Copyright (c) 2003 - 2011 The Weblounge Team
*  http://entwinemedia.com/weblounge
*
*  This program is free software; you can redistribute it and/or
*  modify it under the terms of the GNU Lesser General Public License
*  as published by the Free Software Foundation; either version 2
*  of the License, or (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public License
*  along with this program; if not, write to the Free Software Foundation
*  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package ch.entwine.weblounge.dispatcher.impl.handler;

import ch.entwine.weblounge.common.content.PageSearchResultItem;
import ch.entwine.weblounge.common.content.Renderer;
import ch.entwine.weblounge.common.content.Renderer.RendererType;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.SearchQuery;
import ch.entwine.weblounge.common.content.SearchQuery.Order;
import ch.entwine.weblounge.common.content.SearchResult;
import ch.entwine.weblounge.common.content.SearchResultItem;
import ch.entwine.weblounge.common.content.page.Composer;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.content.page.Pagelet;
import ch.entwine.weblounge.common.content.page.PageletRenderer;
import ch.entwine.weblounge.common.impl.content.SearchQueryImpl;
import ch.entwine.weblounge.common.impl.content.page.ComposerImpl;
import ch.entwine.weblounge.common.impl.request.CacheTagSet;
import ch.entwine.weblounge.common.impl.testing.MockHttpServletRequest;
import ch.entwine.weblounge.common.impl.testing.MockHttpServletResponse;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.request.CacheTag;
import ch.entwine.weblounge.common.request.ResponseCache;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.common.url.WebUrl;
import ch.entwine.weblounge.dispatcher.RequestHandler;
import ch.entwine.weblounge.dispatcher.impl.DispatchUtils;

import com.sun.syndication.feed.atom.Content;
import com.sun.syndication.feed.synd.SyndCategory;
import com.sun.syndication.feed.synd.SyndCategoryImpl;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedOutput;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

/**
* The feed request handler will answer requests that are looking for
* <code>rss</code> or <code>atom</code> feeds for a certain site. The request
* is expected to provide feed type and version as the first two parameters on
* the request path, e. g.
*
* <pre>
*  http://localhost:8080/weblounge-feeds/atom/0.3
* </pre>
*
* <p>
* The request implementation can handle a parameter that specifies one or more
* subjects that may appear in the pages that make up the feed entries. Like
* this, different feeds can be created.
*
* <pre>
*  http://localhost:8080/weblounge-feeds/atom/0.3?subject=a,b,c
* </pre>
*
* </p>
*/
public class FeedRequestHandlerImpl implements RequestHandler {

  /** The subjects parameter name */
  public static final String PARAM_SUBJECT = "subject";

  /** The limit parameter name */
  public static final String PARAM_LIMIT = "limit";

  /** Default value for the <code>limit</code> parameter */
  public static final int DEFAULT_LIMIT = 10;

  /** Alternate uri prefix */
  protected static final String URI_PREFIX = "/weblounge-feeds/";

  /** The site servlets */
  private static Map<String, Servlet> siteServlets = new HashMap<String, Servlet>();

  /** The cache service tracker */
  private ServiceTracker siteServletTracker = null;

  /** Filter expression used to look up site servlets */
  private static final String serviceFilter = "(&(objectclass=" + Servlet.class.getName() + ")(" + Site.class.getName().toLowerCase() + "=*))";

  /** Logging facility */
  private static final Logger logger = LoggerFactory.getLogger(FeedRequestHandlerImpl.class);

  /**
   * Callback from OSGi declarative services on component startup.
   *
   * @param ctx
   *          the component context
   */
  void activate(ComponentContext ctx) {
    try {
      Filter filter = ctx.getBundleContext().createFilter(serviceFilter);
      siteServletTracker = new SiteServletTracker(ctx.getBundleContext(), filter);
      siteServletTracker.open();
    } catch (InvalidSyntaxException e) {
      throw new IllegalStateException(e);
    }
  }

  /**
   * Callback from OSGi declarative services on component shutdown.
   */
  void deactivate() {
    if (siteServletTracker != null) {
      siteServletTracker.close();
    }
  }

  /**
   * Handles the request for a feed of a certain type.
   * <p>
   * This method returns <code>true</code> if the handler is decided to handle
   * the request, <code>false</code> otherwise.
   *
   * @param request
   *          the weblounge request
   * @param response
   *          the weblounge response
   */
  public boolean service(WebloungeRequest request, WebloungeResponse response) {

    Site site = request.getSite();
    WebUrl url = request.getUrl();
    String path = request.getRequestURI();
    String feedType = null;
    String feedVersion = null;

    // Currently, we only support feeds mapped to our well-known uri
    if (!path.startsWith(URI_PREFIX) || !(path.length() > URI_PREFIX.length()))
      return false;

    // Check for feed type and version
    String feedURI = path.substring(URI_PREFIX.length());
    String[] feedURIParts = feedURI.split("/");
    if (feedURIParts.length == 0) {
      logger.debug("Feed request {} does not include feed type", path);
      return false;
    } else if (feedURIParts.length == 1) {
      logger.debug("Feed request {} does not include feed version", path);
      return false;
    }

    // Check the request method. This handler only supports GET
    String requestMethod = request.getMethod().toUpperCase();
    if ("OPTIONS".equals(requestMethod)) {
      String verbs = "OPTIONS,GET";
      logger.trace("Answering options request to {} with {}", url, verbs);
      response.setHeader("Allow", verbs);
      response.setContentLength(0);
      return true;
    } else if (!"GET".equals(requestMethod)) {
      logger.debug("Feed request handler does not support {} requests", url, requestMethod);
      DispatchUtils.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, request, response);
      return true;
    }

    feedType = feedURIParts[0];
    feedVersion = feedURIParts[1];

    // Check for explicit no cache instructions
    boolean noCache = request.getParameter(ResponseCache.NOCACHE_PARAM) != null;

    // Check if the page is already part of the cache. If so, our task is
    // already done!
    if (!noCache) {
      long expirationTime = Renderer.DEFAULT_VALID_TIME;
      long revalidationTime = Renderer.DEFAULT_RECHECK_TIME;

      // Create the set of tags that identify the request output
      CacheTagSet cacheTags = createPrimaryCacheTags(request);

      // Check if the page is already part of the cache
      if (response.startResponse(cacheTags.getTags(), expirationTime, revalidationTime)) {
        logger.debug("Feed handler answered request for {} from cache", request.getUrl());
        return true;
      }
    }

    try {

      // Compile the feed
      SyndFeed feed = createFeed(feedType, feedVersion, site, request, response);
      if (feed == null)
        return true;

      // Set the response type
      String characterEncoding = "utf-8";
      if (feedType.startsWith("atom"))
        response.setContentType("application/atom+xml; charset=" + characterEncoding);
      else if (feedType.startsWith("rss"))
        response.setContentType("application/rss+xml; charset=" + characterEncoding);

      // Set the character encoding
      feed.setEncoding(response.getCharacterEncoding());

      // Set the modification date
      response.setModificationDate(feed.getPublishedDate());

      // Write the feed back to the response

      SyndFeedOutput output = new SyndFeedOutput();
      Writer responseWriter = new OutputStreamWriter(response.getOutputStream(), characterEncoding);
      output.output(feed, responseWriter);
      response.getOutputStream().flush();
      return true;

    } catch (ContentRepositoryException e) {
      logger.error("Error loading articles for feeds from {}: {}", site.getIdentifier(), e.getMessage());
      DispatchUtils.sendInternalError(request, response);
      return true;
    } catch (FeedException e) {
      logger.error("Error creating {} feed: {}", feedType, e.getMessage());
      DispatchUtils.sendInternalError(request, response);
      return true;
    } catch (EOFException e) {
      logger.debug("Error writing feed '{}' back to client: connection closed by client", feedType);
      return true;
    } catch (IOException e) {
      logger.error("Error sending {} feed to the client: {}", feedType, e.getMessage());
      DispatchUtils.sendInternalError(request, response);
      return true;
    } catch (IllegalArgumentException e) {
      logger.debug("Unable to create feed of type '{}': {}", feedType, e.getMessage());
      DispatchUtils.sendNotFound(e.getMessage(), request, response);
      return true;
    } catch (Throwable t) {
      logger.error("Error creating feed of type '{}': {}", feedType, t.getMessage());
      DispatchUtils.sendInternalError(request, response);
      return true;
    } finally {
      response.endResponse();
    }
  }

  /**
   * Compiles the feed based on feed type, version and request parameters.
   *
   * @param feedType
   *          feed type
   * @param feedVersion
   *          feed version
   * @param site
   *          the site
   * @param request
   *          the request
   * @param response
   *          the response
   * @return the feed object
   * @throws ContentRepositoryException
   *           if the content repository can't be accessed
   */
  private SyndFeed createFeed(String feedType, String feedVersion, Site site,
      WebloungeRequest request, WebloungeResponse response)
      throws ContentRepositoryException {

    // Extract the subjects. The parameter may be specified multiple times
    // and add more than one subject by separating them using a comma.
    String[] subjectParameter = request.getParameterValues(PARAM_SUBJECT);
    List<String> subjects = new ArrayList<String>();
    if (subjectParameter != null) {
      for (String parameter : subjectParameter) {
        for (String subject : parameter.split(",")) {
          if (StringUtils.isNotBlank(subject))
            subjects.add(StringUtils.trim(subject));
        }
      }
    }

    // How many entries do we need?
    int limit = DEFAULT_LIMIT;
    String limitParameter = StringUtils.trimToNull(request.getParameter(PARAM_LIMIT));
    if (limitParameter != null) {
      try {
        limit = Integer.parseInt(limitParameter);
      } catch (Throwable t) {
        logger.debug("Non parseable number {} specified as limit", limitParameter);
        limit = DEFAULT_LIMIT;
      }
    }

    // Get hold of the content repository
    ContentRepository contentRepository = site.getContentRepository();
    if (contentRepository == null) {
      logger.warn("No content repository found for site '{}'", site);
      return null;
    } else if (contentRepository.isIndexing()) {
      logger.debug("Content repository of site '{}' is currently being indexed", site);
      DispatchUtils.sendServiceUnavailable(request, response);
      return null;
    }

    // User and language
    Language language = request.getLanguage();
    // User user = request.getUser();

    // Determine the feed type
    feedType = feedType.toLowerCase() + "_" + feedVersion;
    SyndFeed feed = new SyndFeedImpl();
    feed.setFeedType(feedType);
    feed.setLink(request.getRequestURL().toString());
    feed.setTitle(site.getName());
    feed.setDescription(site.getName());
    feed.setLanguage(language.getIdentifier());
    feed.setPublishedDate(new Date());

    // TODO: Add more feed metadata, ask site

    SearchQuery query = new SearchQueryImpl(site);
    query.withVersion(Resource.LIVE);
    query.withTypes(Page.TYPE);
    query.withLimit(limit);
    query.sortByPublishingDate(Order.Descending);
    for (String subject : subjects) {
      query.withSubject(subject);
    }

    // Load the result and add feed entries
    SearchResult result = contentRepository.find(query);
    List<SyndEntry> entries = new ArrayList<SyndEntry>();
    limit = result.getItems().length;

    while (limit > 0) {
      SearchResultItem item = result.getItems()[limit - 1];
      limit--;

      // Get the page
      PageSearchResultItem pageItem = (PageSearchResultItem) item;
      Page page = pageItem.getPage();

      // TODO: Can the page be accessed?

      // Set the page's language to the feed language
      page.switchTo(language);

      // Tag the cache entry
      response.addTag(CacheTag.Resource, page.getIdentifier());

      // If this is to become the most recent entry, let's set the feed's
      // modification date to be that of this entry
      if (entries.size() == 0) {
        feed.setPublishedDate(page.getPublishFrom());
      }

      // Create the entry
      SyndEntry entry = new SyndEntryImpl();
      entry.setPublishedDate(page.getPublishFrom());
      entry.setLink(site.getHostname(request.getEnvironment()).toExternalForm() + item.getUrl().getLink());
      entry.setAuthor(page.getCreator().getName());
      entry.setTitle(page.getTitle());

      // Categories
      if (page.getSubjects().length > 0) {
        List<SyndCategory> categories = new ArrayList<SyndCategory>();
        for (String subject : page.getSubjects()) {
          SyndCategory category = new SyndCategoryImpl();
          category.setName(subject);
          categories.add(category);
        }
        entry.setCategories(categories);
      }

      // TODO: Can the page be accessed?

      // Try to render the preview pagelets and write them to the feed
      List<SyndContent> entryContent = new ArrayList<SyndContent>();
      Composer composer = new ComposerImpl("preview", page.getPreview());

      for (Pagelet pagelet : composer.getPagelets()) {
        Module module = site.getModule(pagelet.getModule());
        PageletRenderer renderer = null;
        if (module == null) {
          logger.warn("Skipping pagelet {} in feed due to missing module '{}'", pagelet, pagelet.getModule());
          continue;
        }

        renderer = module.getRenderer(pagelet.getIdentifier());
        if (renderer == null) {
          logger.warn("Skipping pagelet {} in feed due to missing renderer '{}/{}'", new Object[] { pagelet, pagelet.getModule(), pagelet.getIdentifier() });
          continue;
        }

        URL rendererURL = renderer.getRenderer(RendererType.Feed.toString());
        Environment environment = request.getEnvironment();
        if (rendererURL == null)
          rendererURL = renderer.getRenderer();
        if (rendererURL != null) {
          String rendererContent = null;
          try {
            pagelet.switchTo(language);
            rendererContent = loadContents(rendererURL, site, page, composer, pagelet, environment);
          } catch (ServletException e) {
            logger.warn("Error processing the pagelet renderer at {}: {}", rendererURL, e.getMessage());
            DispatchUtils.sendInternalError(request, response);
          } catch (IOException e) {
            logger.warn("Error processing the pagelet renderer at {}: {}", rendererURL, e.getMessage());
            DispatchUtils.sendInternalError(request, response);
          }
          SyndContent content = new SyndContentImpl();
          content.setType("text/html");
          content.setMode("escaped");
          content.setValue(rendererContent);
          entryContent.add(content);
        }
      }

      if (entryContent.size() > 0) {
        entry.setContents(entryContent);
      }

      entries.add(entry);
    }

    feed.setEntries(entries);

    return feed;
  }

  /**
   * 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(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 Content();
    image.setType("application/xhtml+xml");
    image.setValue(buf.toString());
    return image;
  }

  /**
   * Asks the site servlet to render the given url using the page, composer and
   * pagelet as the rendering environment. If the no servlet is available for
   * the given site, the contents are loaded from the url directly.
   *
   * @param rendererURL
   *          the renderer url
   * @param site
   *          the site
   * @param page
   *          the page
   * @param composer
   *          the composer
   * @param pagelet
   *          the pagelet
   * @param environment
   *          the environment
   * @return the servlet response, serialized to a string
   * @throws IOException
   *           if the servlet fails to create the response
   * @throws ServletException
   *           if an exception occurs while processing
   */
  private String loadContents(URL rendererURL, Site site, Page page,
      Composer composer, Pagelet pagelet, Environment environment)
          throws IOException, ServletException {

    Servlet servlet = siteServlets.get(site.getIdentifier());

    String httpContextURI = UrlUtils.concat("/weblounge-sites", site.getIdentifier());
    int httpContextURILength = httpContextURI.length();
    String url = rendererURL.toExternalForm();
    int uriInPath = url.indexOf(httpContextURI);

    if (uriInPath > 0) {
      String pathInfo = url.substring(uriInPath + httpContextURILength);

      // Prepare the mock request
      MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
      request.setServerName(site.getHostname(environment).getURL().getHost());
      request.setServerPort(site.getHostname(environment).getURL().getPort());
      request.setMethod(site.getHostname(environment).getURL().getProtocol());
      request.setAttribute(WebloungeRequest.PAGE, page);
      request.setAttribute(WebloungeRequest.COMPOSER, composer);
      request.setAttribute(WebloungeRequest.PAGELET, pagelet);
      request.setPathInfo(pathInfo);
      request.setRequestURI(UrlUtils.concat(httpContextURI, pathInfo));

      MockHttpServletResponse response = new MockHttpServletResponse();
      servlet.service(request, response);
      return response.getContentAsString();
    } else {
      InputStream is = null;
      try {
        is = rendererURL.openStream();
        return IOUtils.toString(is, "utf-8");
      } finally {
        IOUtils.closeQuietly(is);
      }
    }
  }

  /**
   * Returns the primary set of cache tags for the given request.
   *
   * @param request
   *          the request
   * @return the cache tags
   */
  protected CacheTagSet createPrimaryCacheTags(WebloungeRequest request) {
    CacheTagSet cacheTags = new CacheTagSet();
    cacheTags.add(CacheTag.Url, request.getUrl().getPath());
    cacheTags.add(CacheTag.Language, request.getLanguage().getIdentifier());
    cacheTags.add(CacheTag.User, request.getUser().getLogin());
    Enumeration<?> pe = request.getParameterNames();
    int parameterCount = 0;
    while (pe.hasMoreElements()) {
      parameterCount++;
      String key = pe.nextElement().toString();
      String[] values = request.getParameterValues(key);
      for (String value : values) {
        cacheTags.add(key, value);
      }
    }
    cacheTags.add(CacheTag.Parameters, Integer.toString(parameterCount));
    return cacheTags;
  }

  /**
   * Adds the site servlet to the list of servlets.
   *
   * @param id
   *          the site identifier
   * @param servlet
   *          the site servlet
   */
  void addSiteServlet(String id, Servlet servlet) {
    logger.debug("Site servlet attached to {} workbench", id);
    siteServlets.put(id, servlet);
  }

  /**
   * Removes the site servlet from the list of servlets
   *
   * @param site
   *          the site identifier
   */
  void removeSiteServlet(String id) {
    logger.debug("Site servlet detached from {} workbench", id);
    siteServlets.remove(id);
  }

  /**
   * Implementation of a <code>ServiceTracker</code> that is tracking instances
   * of type {@link Servlet} with an associated <code>site</code> attribute.
   */
  private class SiteServletTracker extends ServiceTracker {

    /**
     * Creates a new servlet tracker that is using the given bundle context to
     * look up service instances.
     *
     * @param ctx
     *          the bundle context
     * @param filter
     *          the service filter
     */
    SiteServletTracker(BundleContext ctx, Filter filter) {
      super(ctx, filter, null);
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference)
     */
    @Override
    public Object addingService(ServiceReference reference) {
      Servlet servlet = (Servlet) super.addingService(reference);
      String site = (String) reference.getProperty(Site.class.getName().toLowerCase());
      addSiteServlet(site, servlet);
      return servlet;
    }

    /**
     * {@inheritDoc}
     *
     * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference,
     *      java.lang.Object)
     */
    @Override
    public void removedService(ServiceReference reference, Object service) {
      String site = (String) reference.getProperty("site");
      removeSiteServlet(site);
    }

  }

  /**
   * @see ch.entwine.weblounge.dispatcher.api.request.RequestHandler#getName()
   */
  public String getName() {
    return "feed request handler";
  }

  /**
   * Returns a string representation of this request handler.
   *
   * @return the handler name
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    return getName();
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.dispatcher.RequestHandler#getPriority()
   */
  public int getPriority() {
    return 0;
  }

}
TOP

Related Classes of ch.entwine.weblounge.dispatcher.impl.handler.FeedRequestHandlerImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.