/* * 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.ResourceMetadata; import ch.entwine.weblounge.common.content.ResourceSearchResultItem; import ch.entwine.weblounge.common.content.ResourceURI; import ch.entwine.weblounge.common.content.SearchQuery; import ch.entwine.weblounge.common.content.SearchResult; import ch.entwine.weblounge.common.content.SearchResultItem; import ch.entwine.weblounge.common.content.page.Page; import ch.entwine.weblounge.common.content.page.PageTemplate; import ch.entwine.weblounge.common.content.page.PageletRenderer; import ch.entwine.weblounge.common.impl.content.SearchQueryImpl; import ch.entwine.weblounge.common.impl.content.page.PageURIImpl; import ch.entwine.weblounge.common.impl.content.page.PageletImpl; import ch.entwine.weblounge.common.impl.request.CacheTagSet; import ch.entwine.weblounge.common.impl.request.RequestUtils; 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.repository.ResourceSerializer; import ch.entwine.weblounge.common.repository.ResourceSerializerService; import ch.entwine.weblounge.common.request.CacheTag; import ch.entwine.weblounge.common.request.RequestFlavor; import ch.entwine.weblounge.common.request.WebloungeRequest; import ch.entwine.weblounge.common.request.WebloungeResponse; import ch.entwine.weblounge.common.site.HTMLAction; import ch.entwine.weblounge.common.site.Site; import ch.entwine.weblounge.common.url.WebUrl; import ch.entwine.weblounge.dispatcher.RequestHandler; import ch.entwine.weblounge.dispatcher.impl.DispatchUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Date; import java.util.Enumeration; import java.util.List; import java.util.Map.Entry; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.WebApplicationException; /** * This handler is answering search requests. */ public final class SearchRequestHandlerImpl implements RequestHandler { /** Logging facility */ private static final Logger logger = LoggerFactory.getLogger(SearchRequestHandlerImpl.class); /** Alternate uri prefix */ public static final String URI_PREFIX = "/weblounge-search/"; /** Query parameter name */ public static final String PARAM_QUERY = "query"; /** Page limit parameter name */ public static final String PARAM_LIMIT = "limit"; /** Page offset parameter name */ public static final String PARAM_OFFSET = "offset"; /** Generic page data */ public static final String PREVIEW_DATA_KEY = "data"; /** Key to store the search result in the request */ public static final String SEARCH_RESULT = "webl-searchresult"; /** The resource serializer */ private ResourceSerializerService serializerService = null; /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.RequestHandler#service(ch.entwine.weblounge.common.request.WebloungeRequest, * ch.entwine.weblounge.common.request.WebloungeResponse) */ public boolean service(WebloungeRequest request, WebloungeResponse response) { Site site = request.getSite(); WebUrl url = request.getUrl(); RequestFlavor flavor = request.getFlavor(); String path = url.getPath(); if (flavor == null || flavor.equals(ANY)) flavor = RequestFlavor.HTML; // Is this request intended for the search handler? if (!path.startsWith(URI_PREFIX)) { logger.debug("Skipping request for {}, request path does not start with {}", URI_PREFIX); return false; } // Check the request flavor if (!HTML.equals(flavor)) { logger.debug("Skipping request for {}, flavor {} is not supported", path, request.getFlavor()); return false; } // Check the request method. Only GET is supported right now. 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("Search request handler does not support {} requests", requestMethod); DispatchUtils.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, request, response); return true; } int limit = 0; int offset = 0; String queryString = null; // Read the parameters try { limit = RequestUtils.getIntegerParameterWithDefault(request, PARAM_LIMIT, 0); offset = RequestUtils.getIntegerParameterWithDefault(request, PARAM_OFFSET, 0); queryString = RequestUtils.getRequiredParameter(request, PARAM_QUERY); } catch (IllegalStateException e) { logger.debug("Search request handler processing failed: {}", e.getMessage()); DispatchUtils.sendBadRequest(request, response); return true; } // Load the content repository ContentRepository repository = site.getContentRepository(); if (repository == null) { DispatchUtils.sendServiceUnavailable(request, response); return true; } // Create the search expression and the query SearchQuery q = new SearchQueryImpl(site); q.withText(true, queryString); q.withVersion(Resource.LIVE); q.withRececyPriority(); q.withOffset(offset); q.withLimit(limit); q.withTypes(Page.TYPE); // Return the result SearchResult result = null; try { result = repository.find(q); } catch (ContentRepositoryException e) { logger.error("Error trying to access the content repository", e); throw new WebApplicationException(e); } // Load the target page used to render the search result Page page = null; try { page = getTargetPage(request); request.setAttribute(WebloungeRequest.PAGE, page); } catch (ContentRepositoryException e) { logger.error("Error loading target page at {}", url); DispatchUtils.sendInternalError(request, response); return true; } // Get hold of the page template PageTemplate template = null; try { template = getTargetTemplate(page, request); if (template == null) template = site.getDefaultTemplate(); } catch (IllegalStateException e) { logger.warn(e.getMessage()); DispatchUtils.sendInternalError(request, response); return true; } // Identify the stage composer and remove any existing content String stage = template.getStage(); logger.trace("Removing existing pagelets from composer '{}'", stage); while (page.getComposer(stage) != null && page.getComposer(stage).size() > 0) { page.removePagelet(stage, 0); } // Add the search result to the main composer logger.trace("Adding search result to composer '{}'", stage); for (SearchResultItem item : result.getItems()) { Renderer renderer = item.getPreviewRenderer(); // Is this search result coming from the search index or from a module? if (!(item instanceof ResourceSearchResultItem)) { renderer = item.getPreviewRenderer(); if (!(renderer instanceof PageletRenderer)) { logger.warn("Skipping search result '{}' since it's preview renderer is not a pagelet", item); continue; } PageletImpl pagelet = new PageletImpl((PageletRenderer) renderer); pagelet.setContent(item.getContent()); page.addPagelet(pagelet, stage); continue; } // The search result item seems to be coming from the search index // Convert the search result item into a resource search result item ResourceSearchResultItem resourceItem = (ResourceSearchResultItem) item; ResourceURI uri = resourceItem.getResourceURI(); ResourceSerializer<?, ?> serializer = serializerService.getSerializerByType(uri.getType()); if (serializer == null) { logger.debug("Skipping search result since it's type ({}) is unknown", uri.getType()); continue; } // Load the resource Resource<?> resource = serializer.toResource(site, resourceItem.getMetadata()); // Get the renderer and make sure it's a pagelet renderer. First check // the item itself, there may already be a renderer attached. If not, // use the serializer to get the appropriate renderer renderer = item.getPreviewRenderer(); if (renderer == null) { renderer = serializer.getSearchResultRenderer(resource); if (renderer == null) { logger.warn("Skipping search result since a renderer can't be determined"); continue; } } // Create the pagelet PageletRenderer pageletRenderer = (PageletRenderer) renderer; PageletImpl pagelet = new PageletImpl(pageletRenderer); pagelet.setContent(resource); // Add the pagelet's data for (ResourceMetadata<?> metadata : resourceItem.getMetadata()) { String key = metadata.getName(); if (metadata.isLocalized()) { for (Entry<Language, ?> localizedMetadata : metadata.getLocalizedValues().entrySet()) { Language language = localizedMetadata.getKey(); List<Object> values = (List<Object>) localizedMetadata.getValue(); for (Object value : values) { pagelet.setContent(key, value.toString(), language); } } } else { for (Object value : metadata.getValues()) { pagelet.addProperty(key, value.toString()); } } } // TODO: Set modified etc. // Store the pagelet in the page page.addPagelet(pagelet, stage); } // Search results are not being cached // TODO: Implement caching strategy response.setCacheExpirationTime(0); response.setClientRevalidationTime(0); response.setModificationDate(new Date()); // Finally, let's get some work done! try { request.setAttribute(WebloungeRequest.PAGE, page); request.setAttribute(WebloungeRequest.TEMPLATE, template); request.setAttribute(WebloungeRequest.SEARCH, result); response.setContentType("text/html"); logger.trace("Rendering search result on page {}", page); PageRequestHandlerImpl.getInstance().service(request, response); } catch (Throwable e) { logger.error("Error processing search result for {}", request.getUrl()); logger.error(e.getMessage(), e); DispatchUtils.sendInternalError(request, response); } finally { request.removeAttribute(WebloungeRequest.PAGE); } return true; } /** * Returns the primary set of cache tags for the given request. * * @param request * the request * @return the cache tags */ protected CacheTagSet createCacheTags(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 template that will be used to handle this request. If a * template was specified in the request but 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 getTargetTemplate(Page page, WebloungeRequest request) throws IllegalStateException { Site site = request.getSite(); PageTemplate template = null; String templateId = null; // Does the request specify an ad-hoc template? if (request.getAttribute(WebloungeRequest.TEMPLATE) != null) { templateId = (String) request.getAttribute(WebloungeRequest.TEMPLATE); template = site.getTemplate(templateId); if (template == null) throw new IllegalStateException("Page template '" + templateId + "' specified by request attribute was not found"); } // Does the request specify a target template? if (template == null && request.getParameter(HTMLAction.TARGET_TEMPLATE) != null) { templateId = request.getParameter(HTMLAction.TARGET_TEMPLATE); template = site.getTemplate(templateId); if (template == null) throw new IllegalStateException("Page template '" + templateId + "' specified by request parameter was not found"); } // By default, the page will have to deliver on the template if (template == null && page != null) { template = site.getTemplate(page.getTemplate()); if (template == null) throw new IllegalStateException("Page template '" + templateId + "' for page '" + page + "' was not found"); } // Did we end up finding a template? if (template == null) return null; template.setEnvironment(request.getEnvironment()); return template; } /** * Tries to determine the target page for the search result. The * <code>target-page</code> request parameter will be considered, and in any * case, the site's homepage will be the fallback. * <p> * Should a target page be configured through the request and should that url * not be present, this method will return <code>null</code>. * * @param request * the weblounge request * @return the target page * @throws ContentRepositoryException * if the target page cannot be loaded */ protected Page getTargetPage(WebloungeRequest request) throws ContentRepositoryException { ResourceURI target = null; Page page = null; Site site = request.getSite(); boolean targetForced = false; // Check if a target-page parameter was passed String targetPage = request.getParameter(HTMLAction.TARGET_PAGE); if (targetPage != null) { targetForced = true; try { String decocedTargetUrl = null; String encoding = request.getCharacterEncoding(); if (encoding == null) encoding = "utf-8"; decocedTargetUrl = URLDecoder.decode(targetPage, encoding); target = new PageURIImpl(site, decocedTargetUrl); } catch (UnsupportedEncodingException e) { logger.warn("Error while decoding target url {}: {}", targetPage, e.getMessage()); target = new PageURIImpl(site, "/"); } } // Nothing found, let's choose the site's homepage if (target == null) { target = new PageURIImpl(site, "/"); } // We are about to render the action output in the composers of the target // page. This is why we have to make sure that this target page exists, // otherwise the user will get a 404. ContentRepository contentRepository = site.getContentRepository(); if (contentRepository == null) { logger.warn("Content repository not available to read target page {}", target); return null; } // Does the page exist? page = (Page) contentRepository.get(target); if (page == null) { if (targetForced) { logger.warn("Output of search request is configured to render on non existing page {}", target); return null; } // Fall back to site homepage target = new PageURIImpl(site, "/"); page = (Page) contentRepository.get(target); if (page == null) { logger.debug("Site {} has no homepage as fallback to render search result", site); return null; } } return page; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.RequestHandler#getPriority() */ public int getPriority() { return 0; } /** * {@inheritDoc} * * @see ch.entwine.weblounge.dispatcher.RequestHandler#getName() */ public String getName() { return "search request handler"; } /** * OSGi callback that is setting the resource serializer. * * @param serializer * the resource serializer service */ void setResourceSerializer(ResourceSerializerService serializer) { this.serializerService = serializer; } /** * {@inheritDoc} * * @see java.lang.Object#toString() */ @Override public String toString() { return getName(); } }