Java tutorial
/* * 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>(); int items = Math.min(limit, result.getItems().length); for (int i = 0; i < items; i++) { SearchResultItem item = result.getItems()[i]; // 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.setUpdatedDate(page.getModificationDate()); entry.setLink(site.getHostname(request.getEnvironment()).toExternalForm() + item.getUrl().getLink()); entry.setAuthor(page.getCreator().getName()); entry.setTitle(page.getTitle()); entry.setUri(page.getIdentifier()); // 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); } // 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()); StringBuffer renderedContent = new StringBuffer(); 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 pageletContent = null; try { pagelet.switchTo(language); pageletContent = loadContents(rendererURL, site, page, composer, pagelet, environment); renderedContent.append(pageletContent); } 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); } } } if (renderedContent.length() > 0) { SyndContent content = new SyndContentImpl(); content.setType("text/html"); content.setMode("escaped"); content.setValue(renderedContent.toString()); entryContent.add(content); 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; } }