/*
* 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 static ch.entwine.weblounge.common.request.RequestFlavor.ANY;
import static ch.entwine.weblounge.common.request.RequestFlavor.HTML;
import ch.entwine.weblounge.common.content.Renderer;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.page.HTMLHeadElement;
import ch.entwine.weblounge.common.content.page.HTMLInclude;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.content.page.PageTemplate;
import ch.entwine.weblounge.common.impl.content.page.PageURIImpl;
import ch.entwine.weblounge.common.impl.request.CacheTagSet;
import ch.entwine.weblounge.common.impl.request.Http11Constants;
import ch.entwine.weblounge.common.impl.request.Http11Utils;
import ch.entwine.weblounge.common.impl.request.RequestUtils;
import ch.entwine.weblounge.common.impl.request.WebloungeRequestImpl;
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.RequestFlavor;
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.security.User;
import ch.entwine.weblounge.common.site.Action;
import ch.entwine.weblounge.common.site.HTMLAction;
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.PageRequestHandler;
import ch.entwine.weblounge.dispatcher.RequestHandler;
import ch.entwine.weblounge.dispatcher.impl.DispatchUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.EOFException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
import javax.servlet.http.HttpServletResponse;
/**
* The <code>PageRequestHandler</code> is used to handle requests to urls simply
* mapped to a certain template. The request handler will verify access rights
* and then simply forward the request to the template handler.
*/
public final class PageRequestHandlerImpl implements PageRequestHandler {
/** Logging facility */
protected static final Logger logger = LoggerFactory.getLogger(PageRequestHandlerImpl.class);
/** Alternate uri prefix */
protected static final String URI_PREFIX = "/weblounge-pages/";
/** The singleton handler instance */
private static final PageRequestHandlerImpl handler = new PageRequestHandlerImpl();
/**
* Handles the request for a simple url available somewhere in the system. The
* handler sets the response type, does the url history and then forwards
* request to the corresponding JSP page or XSLT stylesheet.
* <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) {
logger.debug("Page handler agrees to handle {}", request.getUrl());
Mode processingMode = Mode.Default;
WebUrl url = request.getUrl();
String path = url.getPath();
RequestFlavor contentFlavor = request.getFlavor();
if (contentFlavor == null || contentFlavor.equals(ANY))
contentFlavor = RequestFlavor.HTML;
// Check the request flavor
// TODO: Criteria would be loading the page from the repository
// TODO: Think about performance, page lookup is expensive
if (!HTML.equals(contentFlavor)) {
logger.debug("Skipping request for {}, flavor {} is not supported", path, request.getFlavor());
return false;
}
// Determine the editing state
boolean isEditing = RequestUtils.isEditingState(request);
// Check if the request is controlled by an action.
Action action = (Action) request.getAttribute(WebloungeRequest.ACTION);
// Get the renderer id that has been registered with the url. For this,
// we first have to load the page data, then get the associated renderer
// bundle.
try {
Page page = null;
ResourceURI pageURI = null;
Site site = request.getSite();
// Check if a page was passed as an attribute
if (request.getAttribute(WebloungeRequest.PAGE) != null) {
page = (Page) request.getAttribute(WebloungeRequest.PAGE);
pageURI = page.getURI();
}
// Load the page from the content repository
else {
ContentRepository contentRepository = site.getContentRepository();
if (contentRepository == null) {
logger.debug("No content repository found for site '{}'", site);
return false;
} else if (contentRepository.isIndexing()) {
logger.debug("Content repository of site '{}' is currently being indexed", site);
DispatchUtils.sendServiceUnavailable(request, response);
return true;
}
ResourceURI requestURI = null;
ResourceURI requestedURI = null;
// Load the page. Note that we are taking care of the special case where
// a user may have created a page with a url that matches a valid
// language identifier, in which case it would have been stripped from
// request.getUrl().
try {
if (action != null) {
pageURI = getPageURIForAction(action, request);
requestURI = pageURI;
} else if (path.startsWith(URI_PREFIX)) {
String uriSuffix = StringUtils.substringBefore(path.substring(URI_PREFIX.length()), "/");
uriSuffix = URLDecoder.decode(uriSuffix, "utf-8");
ResourceURI uri = new PageURIImpl(site, null, uriSuffix, request.getVersion());
requestURI = uri;
WebUrl requestedUrl = request.getRequestedUrl();
if (requestedUrl.hasLanguagePathSegment()) {
String requestedPath = UrlUtils.concat(path, request.getLanguage().getIdentifier());
String requestedUriSuffix = StringUtils.substringBefore(requestedPath.substring(URI_PREFIX.length()), "/");
requestedUriSuffix = URLDecoder.decode(requestedUriSuffix, "utf-8");
requestedURI = new PageURIImpl(site, requestedUriSuffix, null, request.getVersion());
}
} else {
long version = isEditing ? Resource.WORK : Resource.LIVE;
ResourceURI uri = new PageURIImpl(request);
uri.setVersion(version);
requestURI = uri;
WebUrl requestedUrl = request.getRequestedUrl();
if (requestedUrl.hasLanguagePathSegment()) {
String requestedPath = UrlUtils.concat(path, request.getLanguage().getIdentifier());
requestedPath = URLDecoder.decode(requestedPath, "utf-8");
requestedURI = new PageURIImpl(site, requestedPath, null, version);
}
}
// Is this a request with potential path clashes?
if (requestedURI != null) {
long version = requestedURI.getVersion();
if (contentRepository.existsInAnyVersion(requestedURI)) {
if (!isEditing && version == Resource.LIVE && contentRepository.exists(requestedURI)) {
pageURI = requestedURI;
((WebloungeRequestImpl) request).setLanguage(request.getSessionLanguage());
} else if (isEditing && version == Resource.WORK && !contentRepository.exists(requestedURI)) {
requestedURI.setVersion(Resource.LIVE);
pageURI = requestedURI;
((WebloungeRequestImpl) request).setLanguage(request.getSessionLanguage());
} else if (isEditing && version == Resource.WORK && !contentRepository.exists(requestedURI)) {
pageURI = requestedURI;
((WebloungeRequestImpl) request).setLanguage(request.getSessionLanguage());
}
}
}
// Does the page exist?
if (pageURI == null && contentRepository.existsInAnyVersion(requestURI)) {
long version = requestURI.getVersion();
// If the work version is requested, we need to make sure
// a) it exists and b) the user is in editing mode
if (version == Resource.WORK && isEditing) {
if (contentRepository.exists(requestURI)) {
pageURI = requestURI;
} else {
requestURI.setVersion(Resource.LIVE);
if (contentRepository.exists(requestURI))
pageURI = requestURI;
}
} else if (contentRepository.exists(requestURI)) {
pageURI = requestURI;
}
}
// Did we find a matching uri?
if (pageURI == null) {
DispatchUtils.sendNotFound(request, response);
return true;
}
page = (Page) contentRepository.get(pageURI);
if (page == null) {
DispatchUtils.sendNotFound(request, response);
return true;
}
} catch (ContentRepositoryException e) {
logger.error("Unable to load page {} from {}: {}", new Object[] {
pageURI,
contentRepository,
e.getMessage(),
e });
DispatchUtils.sendInternalError(request, response);
return true;
}
}
// Check the request method. This handler only supports GET, POST and
// OPTIONS
String requestMethod = request.getMethod().toUpperCase();
if ("OPTIONS".equals(requestMethod)) {
String verbs = "OPTIONS, GET, POST";
logger.trace("Answering options request to {} with {}", url, verbs);
response.setHeader("Allow", verbs);
response.setContentLength(0);
return true;
} else if (!"GET".equals(requestMethod) && !"POST".equals(requestMethod) && !RequestUtils.containsAction(request)) {
logger.debug("Url {} does not handle {} requests", url, requestMethod);
DispatchUtils.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, request, response);
return true;
}
// Is it published?
if (!page.isPublished() && !(page.getVersion() == Resource.WORK)) {
logger.debug("Access to unpublished page {}", pageURI);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return true;
}
// Can the page be accessed by the current user?
User user = request.getUser();
try {
// TODO: Check permission
// PagePermission p = new PagePermission(page, user);
// AccessController.checkPermission(p);
} catch (SecurityException e) {
logger.warn("Accessed to page {} denied for user {}", pageURI, user);
DispatchUtils.sendAccessDenied(request, response);
return true;
}
// Check for explicit no cache instructions
boolean ignoreCache = request.getParameter(ResponseCache.NOCACHE_PARAM) != null;
// Check if the page is already part of the cache. If so, our task is
// already done!
if (!ignoreCache && request.getVersion() == Resource.LIVE && !isEditing) {
// Create the set of tags that identify the page
CacheTagSet cacheTags = createPrimaryCacheTags(request);
if (action == null) {
long expirationTime = Renderer.DEFAULT_VALID_TIME;
long revalidationTime = Renderer.DEFAULT_RECHECK_TIME;
// Check if the page is already part of the cache
if (response.startResponse(cacheTags.getTags(), expirationTime, revalidationTime)) {
logger.debug("Page handler answered request for {} from cache", request.getUrl());
return true;
}
}
processingMode = Mode.Cached;
cacheTags.add(CacheTag.Resource, page.getURI().getIdentifier());
response.addTags(cacheTags);
} else if (Http11Constants.METHOD_HEAD.equals(requestMethod)) {
// handle HEAD requests
Http11Utils.startHeadResponse(response);
processingMode = Mode.Head;
} else if (request.getVersion() == Resource.WORK) {
response.setCacheExpirationTime(0);
}
// Set the default maximum render and valid times for pages
response.setClientRevalidationTime(Renderer.DEFAULT_RECHECK_TIME);
response.setCacheExpirationTime(Renderer.DEFAULT_VALID_TIME);
// Store the page in the request
request.setAttribute(WebloungeRequest.PAGE, page);
// Get hold of the page template
PageTemplate template = null;
try {
template = getPageTemplate(page, request);
template.setEnvironment(request.getEnvironment());
} catch (IllegalStateException e) {
logger.debug(e.getMessage());
DispatchUtils.sendInternalError(request, response);
return true;
}
// Does the template support the requested flavor?
if (!template.supportsFlavor(contentFlavor)) {
logger.warn("Template '{}' does not support requested flavor {}", template, contentFlavor);
DispatchUtils.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, request, response);
return true;
}
// Suggest a last modified data. Note that this may not be the final date
// as the page may contain content embedded from other pages that feature
// more recent modification dates
response.setModificationDate(page.getLastModified());
// Set the content type
String characterEncoding = response.getCharacterEncoding();
if (StringUtils.isNotBlank(characterEncoding))
response.setContentType("text/html; charset=" + characterEncoding.toLowerCase());
else
response.setContentType("text/html");
// Add the template's HTML header elements to the response if it's not
// only used in editing mode
for (HTMLHeadElement header : template.getHTMLHeaders()) {
if (!HTMLInclude.Use.Editor.equals(header.getUse()))
response.addHTMLHeader(header);
}
// Select the actual renderer by method and have it render the
// request. Since renderers are being pooled by the bundle, we
// have to return it after the request has finished.
try {
logger.debug("Rendering {} using page template '{}'", path, template);
template.render(request, response);
} catch (Throwable t) {
String params = RequestUtils.dumpParameters(request);
String msg = "Error rendering template '" + template + "' on '" + path + "' " + params;
Throwable o = t.getCause();
if (o != null) {
msg += ": " + o.getMessage();
logger.error(msg, o);
} else {
logger.error(msg, t);
}
DispatchUtils.sendInternalError(request, response);
}
return true;
} catch (EOFException e) {
logger.debug("Error writing page '{}' back to client: connection closed by client", url);
return true;
} catch (IOException e) {
logger.error("I/O exception while sending error status: {}", e.getMessage(), e);
return true;
} finally {
if (action == null) {
switch (processingMode) {
case Cached:
response.endResponse();
break;
case Head:
Http11Utils.endHeadResponse(response);
break;
default:
break;
}
}
}
}
/**
* Tries to determine the target page for the action result. The
* <code>{@link HTMLAction.TARGET}</code> request attribute and parameter will
* be considered. In any case, the site's homepage will be the fallback.
*
* @param action
* the action handler
* @param request
* the weblounge request
* @return the target page
*/
protected ResourceURI getPageURIForAction(Action action,
WebloungeRequest request) {
ResourceURI target = null;
Site site = request.getSite();
// Check if a target-page parameter was passed
if (request.getParameter(HTMLAction.TARGET_PAGE) != null) {
String targetUrl = request.getParameter(HTMLAction.TARGET_PAGE);
try {
String decocedTargetUrl = null;
String encoding = request.getCharacterEncoding();
if (encoding == null)
encoding = "utf-8";
decocedTargetUrl = URLDecoder.decode(targetUrl, encoding);
target = new PageURIImpl(site, decocedTargetUrl);
} catch (UnsupportedEncodingException e) {
logger.warn("Error while decoding target url {}: {}", targetUrl, e.getMessage());
target = new PageURIImpl(site, "/");
}
}
// Nothing found, let's choose the site's homepage
if (target == null) {
target = new PageURIImpl(site, "/");
}
return target;
}
/**
* Returns the template that will be used to handle this request. If the
* template cannot be found or used for some reason, an
* {@link IllegalStateException} is thrown.
*
* @param page
* the page
* @param request
* the request
* @return the template
* @throws IllegalStateException
* if the template cannot be found
*/
protected PageTemplate getPageTemplate(Page page, WebloungeRequest request)
throws IllegalStateException {
Site site = request.getSite();
// Has a template been defined already (e. g. by an action handler)?
PageTemplate template = (PageTemplate) request.getAttribute(WebloungeRequest.TEMPLATE);
// Apparently not...
if (template == null) {
String templateId = page.getTemplate();
template = site.getTemplate(templateId);
if (template == null) {
logger.warn("Page {} specified a non-existing template '{}'", page.getURI(), templateId);
template = site.getDefaultTemplate();
}
}
template.setEnvironment(request.getEnvironment());
return template;
}
/**
* 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.Url, request.getRequestedUrl().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;
}
/**
* Returns the singleton instance of this class.
*
* @return the request handler instance
*/
public static RequestHandler getInstance() {
return handler;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.dispatcher.RequestHandler#getName()
*/
public String getName() {
return "page 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 -1;
}
}