Java tutorial
/* * Copyright 2013 david gonzalez. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.activecq.tools.errorpagehandler.impl; import com.activecq.tools.errorpagehandler.ErrorPageHandlerService; import com.day.cq.commons.PathInfo; import com.day.cq.search.PredicateGroup; import com.day.cq.search.Query; import com.day.cq.search.QueryBuilder; import com.day.cq.search.eval.JcrPropertyPredicateEvaluator; import com.day.cq.search.eval.NodenamePredicateEvaluator; import com.day.cq.search.eval.TypePredicateEvaluator; import com.day.cq.search.result.Hit; import com.day.cq.search.result.SearchResult; import com.day.cq.wcm.api.WCMMode; import org.apache.commons.collections.IteratorUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.*; import org.apache.felix.scr.annotations.Properties; import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.util.ISO9075; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestProgressTracker; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.commons.osgi.PropertiesUtil; import org.apache.sling.engine.auth.Authenticator; import org.apache.sling.engine.auth.NoAuthenticationHandlerException; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.servlet.ServletException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; import java.util.AbstractMap.SimpleEntry; /** * * @author david */ @Component(label = "ActiveCQ - Error Page Handler", description = "Error Page Handling module which facilitates the resolution of errors against authorable pages for discrete content trees.", immediate = false, metatype = true) @Properties({ @Property(name = "service.vendor", value = "ActiveCQ") }) @Service public class ErrorPageHandlerImpl implements ErrorPageHandlerService { @SuppressWarnings("unused") private static final Logger log = LoggerFactory.getLogger(ErrorPageHandlerImpl.class); private static final String USER_AGENT = "User-Agent"; private static final String MOZILLA = "Mozilla"; private static final String OPERA = "Opera"; public static final String DEFAULT_ERROR_PAGE_NAME = "errors"; public static final String ERROR_PAGE_PROPERTY = "errorPages"; /* Enable/Disable */ private static final boolean DEFAULT_ENABLED = true; private boolean enabled = DEFAULT_ENABLED; @Property(label = "Enable", description = "Enables/Disables the error handler. [Required]", boolValue = DEFAULT_ENABLED) private static final String PROP_ENABLED = "prop.enabled"; /* Error Page Extension */ private static final String DEFAULT_ERROR_PAGE_EXTENSION = "html"; private String errorPageExtension = DEFAULT_ERROR_PAGE_EXTENSION; @Property(label = "Error page extension", description = "Examples: html, htm, xml, json. [Optional] [Default: html]", value = DEFAULT_ERROR_PAGE_EXTENSION) private static final String PROP_ERROR_PAGE_EXTENSION = "prop.error-page.extension"; /* Fallback Error Code Extension */ private static final String DEFAULT_FALLBACK_ERROR_NAME = "500"; private String fallbackErrorName = DEFAULT_FALLBACK_ERROR_NAME; @Property(label = "Fallback error page name", description = "Error page name (not path) to use if a valid Error Code/Error Servlet Name cannot be retrieved from the Request. [Required] [Default: 500]", value = DEFAULT_FALLBACK_ERROR_NAME) private static final String PROP_FALLBACK_ERROR_NAME = "prop.error-page.fallback-name"; /* System Error Page Path */ private static final String DEFAULT_SYSTEM_ERROR_PAGE_PATH_DEFAULT = ""; private String systemErrorPagePath = DEFAULT_SYSTEM_ERROR_PAGE_PATH_DEFAULT; @Property(label = "System error page", description = "Absolute path to system Error page resource to serve if no other more appropriate error pages can be found. Does not include extension. [Optional... but highly recommended]", value = DEFAULT_SYSTEM_ERROR_PAGE_PATH_DEFAULT) private static final String PROP_ERROR_PAGE_PATH = "prop.error-page.system-path"; /* Search Paths */ private static final String[] DEFAULT_SEARCH_PATHS = {}; @Property(label = "Error page paths", description = "List of valid inclusive content trees under which error pages may reside, along with the name of the the default error page for the content tree. Example: /content/geometrixx/en:errors [Optional]", cardinality = Integer.MAX_VALUE) private static final String PROP_SEARCH_PATHS = "prop.paths"; @Reference private QueryBuilder queryBuilder; @Reference private Authenticator authenticator; private SortedMap<String, String> pathMap = new TreeMap<String, String>(); /** * Find the full path to the most appropriate Error Page * * @param request * @param errorResource * @return */ @Override public String findErrorPage(SlingHttpServletRequest request, Resource errorResource) { if (!isEnabled()) { return null; } Resource page = null; final ResourceResolver resourceResolver = errorResource.getResourceResolver(); // Get error page name to look for based on the error code/name final String pageName = getErrorPageName(request); // Try to find the closest real parent for the requested resource final Resource parent = findFirstRealParentOrSelf(errorResource); final SortedMap<String, String> errorPagesMap = getErrorPagesMap(resourceResolver); if (!errorPagesMap.isEmpty()) { // Get the best-matching Errors Path for this particular Request final String errorsPath = this.getErrorPagesPath(parent, errorPagesMap); if (StringUtils.isNotBlank(errorsPath)) { // Search for CQ Page for specific servlet named Page (404, 500, Throwable, etc.) SearchResult result = executeQuery(resourceResolver, pageName); List<String> errorPaths = filterResults(errorsPath, result); // Return the first existing match for (String errorPath : errorPaths) { page = getResource(resourceResolver, errorPath); if (page != null) { break; } } // No error-specific page could be found, use the "default" error page // for the Root content path if (page == null && StringUtils.isNotBlank(errorsPath)) { page = resourceResolver.resolve(errorsPath); } } } if (page == null || ResourceUtil.isNonExistingResource(page)) { // If no error page could be found if (this.hasSystemErrorPage()) { final String errorPage = applyExtension(this.getSystemErrorPagePath()); log.debug("Using default error page: {}", errorPage); return StringUtils.stripToNull(errorPage); } } else { final String errorPage = applyExtension(page.getPath()); log.debug("Using resolved error page: {}", errorPage); return StringUtils.stripToNull(errorPage); } return null; } /** * Create the query for finding candidate cq:Pages * * @param resourceResolver * @param pageNames * @return */ private SearchResult executeQuery(ResourceResolver resourceResolver, String... pageNames) { final Session session = resourceResolver.adaptTo(Session.class); final Map<String, String> map = new HashMap<String, String>(); if (pageNames == null) { pageNames = new String[] {}; } // Construct query builder query map.put(TypePredicateEvaluator.TYPE, "cq:Page"); if (pageNames.length == 1) { map.put(NodenamePredicateEvaluator.NODENAME, escapeNodeName(pageNames[0])); } else if (pageNames.length > 1) { map.put("group.p.or", "true"); for (int i = 0; i < pageNames.length; i++) { map.put("group." + String.valueOf(i) + "_" + NodenamePredicateEvaluator.NODENAME, escapeNodeName(pageNames[i])); } } final Query query = queryBuilder.createQuery(PredicateGroup.create(map), session); return query.getResult(); } /** * Gets the resource object for the provided path. * * Performs checks to ensure resource exists and is accessible to user. * * @param resourceResolver * @param path * @return */ private Resource getResource(ResourceResolver resourceResolver, String path) { // Double check that the resource exists and return it as a match final Resource resource = resourceResolver.getResource(path); if (resource != null && !ResourceUtil.isNonExistingResource(resource)) { return resource; } return null; } /** * Filter query results * * @param rootPath * @param result * @return list of resource paths of candidate error pages */ private List<String> filterResults(String rootPath, SearchResult result) { final List<Node> nodes = IteratorUtils.toList(result.getNodes()); final List<String> resultPaths = new ArrayList<String>(); if (StringUtils.isBlank(rootPath)) { return resultPaths; } // Filter results by the searchResource path; All valid results' paths should begin // with searchResource.getPath() for (Node node : nodes) { if (node == null) { continue; } try { // Make sure all query results under or equals to the current Search Resource if (StringUtils.equals(node.getPath(), rootPath) || StringUtils.startsWith(node.getPath(), rootPath.concat("/"))) { resultPaths.add(node.getPath()); } } catch (RepositoryException ex) { log.warn("Could not get path for node. {}", ex.getMessage()); // continue } } return resultPaths; } /** HTTP Request Data Retrieval Methods **/ /** * Get Error Status Code from Request * * @param request * @return */ public int getStatusCode(SlingHttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute(ErrorPageHandlerService.STATUS_CODE); if (statusCode != null) { return statusCode; } else { return ErrorPageHandlerService.DEFAULT_STATUS_CODE; } } /** * * * @param request * @return */ public String getErrorPageName(SlingHttpServletRequest request) { // Get status code from request // Set the servlet name ot find to statusCode; update later if needed String servletName = String.valueOf(getStatusCode(request)); if (StringUtils.isBlank(servletName)) { servletName = this.fallbackErrorName; } final String servletPath = (String) request.getAttribute(ErrorPageHandlerService.SERVLET_NAME); if (StringUtils.isBlank(servletPath)) { return servletName; } try { final PathInfo pathInfo = new PathInfo(servletPath); final String[] parts = StringUtils.split(pathInfo.getResourcePath(), '/'); if (parts.length > 0) { servletName = parts[parts.length - 1]; } } catch (IllegalArgumentException ex) { // Use status code } return StringUtils.lowerCase(servletName); } private SortedMap<String, String> getErrorPagesMap(ResourceResolver resourceResolver) { final Session session = resourceResolver.adaptTo(Session.class); Map<String, String> map = new HashMap<String, String>(); SortedMap<String, String> authoredMap = new TreeMap<String, String>(new StringLengthComparator()); // Construct query builder query map.put(TypePredicateEvaluator.TYPE, "cq:Page"); map.put(JcrPropertyPredicateEvaluator.PROPERTY, JcrConstants.JCR_CONTENT + "/" + ERROR_PAGE_PROPERTY); map.put(JcrPropertyPredicateEvaluator.PROPERTY + "." + JcrPropertyPredicateEvaluator.OPERATION, JcrPropertyPredicateEvaluator.OP_EXISTS); map.put("p.limit", "0"); final Query query = queryBuilder.createQuery(PredicateGroup.create(map), session); for (final Hit hit : query.getResult().getHits()) { try { final Resource contentResource = hit.getResource().getChild(JcrConstants.JCR_CONTENT); final ValueMap properties = contentResource.adaptTo(ValueMap.class); final String errorPagePath = properties.get(ERROR_PAGE_PROPERTY, String.class); if (StringUtils.isBlank(errorPagePath)) { continue; } final Resource errorPageResource = resourceResolver.resolve(errorPagePath); if (errorPageResource != null && !ResourceUtil.isNonExistingResource(errorPageResource)) { authoredMap.put(hit.getPath(), errorPagePath); } } catch (RepositoryException ex) { log.error("Could not resolve hit to a valid resource"); } } return mergeMaps(authoredMap, this.pathMap); } /** OSGi Component Property Getters/Setters **/ /** * * @return */ public boolean isEnabled() { return enabled; } /** * Checks if the System Error Page has been configured * * @return */ public boolean hasSystemErrorPage() { return StringUtils.isNotBlank(this.getSystemErrorPagePath()); } /** * Get the configured System Error Page Path * @return */ public String getSystemErrorPagePath() { return StringUtils.strip(this.systemErrorPagePath); } /** * Get configured error page extension * * @return */ public String getErrorPageExtension() { return StringUtils.stripToEmpty(this.errorPageExtension); } /** * Get the sorted Search Paths * * @return */ private List<String> getRootPaths(Map<String, String> errorPagesMap) { return Arrays.asList(errorPagesMap.keySet().toArray(new String[errorPagesMap.size()])); } /** * Gets the Error Pages Path for the provided content root path * * @param rootPath * @param errorPagesMap * @return */ public String getErrorPagesPath(String rootPath, Map<String, String> errorPagesMap) { if (errorPagesMap.containsKey(rootPath)) { return errorPagesMap.get(rootPath); } else { return null; } } /** * Find the Error page search path that best contains the provided resource * * @param resource * @return */ private String getErrorPagesPath(Resource resource, SortedMap<String, String> errorPagesMap) { // Path to evaluate against Root paths final String path = resource.getPath(); final ResourceResolver resourceResolver = resource.getResourceResolver(); for (final String rootPath : this.getRootPaths(errorPagesMap)) { if (StringUtils.equals(path, rootPath) || StringUtils.startsWith(path, rootPath.concat("/"))) { final String errorPagePath = getErrorPagesPath(rootPath, errorPagesMap); Resource errorPageResource = getResource(resourceResolver, errorPagePath); if (errorPageResource != null && !ResourceUtil.isNonExistingResource(errorPageResource)) { return errorPageResource.getPath(); } } } return null; } /** * Given the Request path, find the first Real Parent of the Request (even if the resource doesnt exist) * * @param resource * @return */ private Resource findFirstRealParentOrSelf(Resource resource) { if (resource != null && !ResourceUtil.isNonExistingResource(resource)) { return resource; } try { final Resource parent = resource.getParent(); if (parent != null) { return parent; } } catch (NullPointerException ex) { // continue } final ResourceResolver resourceResolver = resource.getResourceResolver(); final String path = resource.getPath(); final PathInfo pathInfo = new PathInfo(path); String[] parts = StringUtils.split(pathInfo.getResourcePath(), '/'); for (int i = parts.length - 1; i >= 0; i--) { String[] tmpArray = (String[]) ArrayUtils.subarray(parts, 0, i); String tmpStr = "/".concat(StringUtils.join(tmpArray, '/')); final Resource tmpResource = resourceResolver.getResource(tmpStr); if (tmpResource != null) { return tmpResource; } } return null; } /** * Add extension as configured via OSGi Component Property * * Defaults to .html * * @param path * @return */ private String applyExtension(String path) { if (path == null) { return null; } String ext = getErrorPageExtension(); if (StringUtils.isBlank(ext)) { return path; } return StringUtils.stripToEmpty(path).concat(".").concat(ext); } /** * Escapes JCR node names for search; Especially important for nodes that start with numbers * * @param name * @return */ private String escapeNodeName(String name) { name = StringUtils.stripToNull(name); if (name == null) { return ""; } return ISO9075.encode(name); } /** Script Support Methods **/ /** * Determins if the request has been authenticated or is Anonymous * * @param request * @return */ protected boolean isAnonymousRequest(SlingHttpServletRequest request) { return (request.getAuthType() == null || request.getRemoteUser() == null); } /** * Determines if the request originated from a Browser * * @param request * @return */ protected boolean isBrowserRequest(SlingHttpServletRequest request) { final String userAgent = request.getHeader(USER_AGENT); return !StringUtils.isBlank(userAgent) && (StringUtils.contains(userAgent, MOZILLA) || StringUtils.contains(userAgent, OPERA)); } /** * Determines if the Request is to Author * * @param request * @return */ @Override public boolean isAuthorModeRequest(SlingHttpServletRequest request) { final WCMMode mode = WCMMode.fromRequest(request); return (mode != null && !WCMMode.DISABLED.equals(mode)); } /** * Determines is the Request is to Author in Preview mode * * @param request * @return true if Request is to an Author in Preview */ @Override public boolean isAuthorPreviewModeRequest(SlingHttpServletRequest request) { final WCMMode mode = WCMMode.fromRequest(request); return WCMMode.PREVIEW.equals(mode); } /** * Attempts to invoke a valid Sling Authentication Handler for the request * * @param request * @param response */ protected void authenticateRequest(SlingHttpServletRequest request, SlingHttpServletResponse response) { if (authenticator == null) { log.warn("Cannot login: Missing Authenticator service"); return; } try { authenticator.login(request, response); } catch (NoAuthenticationHandlerException ex) { log.warn("Cannot login: No Authentication Handler is willing to authenticate"); } } /** * Determine and handle 404 Requests * * @param request * @param response */ @Override public void doHandle404(SlingHttpServletRequest request, SlingHttpServletResponse response) { if (!isAuthorModeRequest(request)) { return; } else if (getStatusCode(request) != SlingHttpServletResponse.SC_NOT_FOUND) { return; } if (isAnonymousRequest(request) && isBrowserRequest(request)) { authenticateRequest(request, response); } } /** * Returns the Exception Message (Stacktrace) from the Request * * @param request * @return */ @Override public String getException(SlingHttpServletRequest request) { StringWriter stringWriter = new StringWriter(); if (request.getAttribute(SlingConstants.ERROR_EXCEPTION) instanceof Throwable) { Throwable throwable = (Throwable) request.getAttribute(SlingConstants.ERROR_EXCEPTION); if (throwable == null) { return ""; } if (throwable instanceof ServletException) { ServletException se = (ServletException) throwable; while (se.getRootCause() != null) { throwable = se.getRootCause(); if (throwable instanceof ServletException) { se = (ServletException) throwable; } else { break; } } } throwable.printStackTrace(new PrintWriter(stringWriter, true)); } return stringWriter.toString(); } /** * Returns a String representation of the RequestProgress trace * * @param request * @return */ @Override public String getRequestProgress(SlingHttpServletRequest request) { StringWriter stringWriter = new StringWriter(); if (request != null) { RequestProgressTracker tracker = request.getRequestProgressTracker(); tracker.dump(new PrintWriter(stringWriter, true)); } return stringWriter.toString(); } @Override public void resetRequestAndResponse(SlingHttpServletRequest request, SlingHttpServletResponse response, int statusCode) { // Clear client libraries request.setAttribute(com.day.cq.widget.HtmlLibraryManager.class.getName() + ".included", new java.util.HashSet<String>()); // Clear the response response.reset(); response.setStatus(statusCode); } /** * Merge two Maps together. In the event of any key collisions the Master map wins * * Any blank value keys are dropped from the final Map * * Map is sorted by value (String) length * * @param master * @param slave * @return */ private SortedMap<String, String> mergeMaps(SortedMap<String, String> master, SortedMap<String, String> slave) { SortedMap<String, String> map = new TreeMap<String, String>(new StringLengthComparator()); for (final String key : master.keySet()) { if (StringUtils.isNotBlank(master.get(key))) { map.put(key, master.get(key)); } } for (final String key : slave.keySet()) { if (master.containsKey(key)) { continue; } if (StringUtils.isNotBlank(slave.get(key))) { map.put(key, slave.get(key)); } } return map; } /** * Util for parsing Service properties in the form >value<>separator<>value< * * @param value * @param separator * @return */ private AbstractMap.SimpleEntry toSimpleEntry(String value, String separator) { String[] tmp = StringUtils.split(value, separator); if (tmp == null) { return null; } if (tmp.length == 2) { return new AbstractMap.SimpleEntry(tmp[0], tmp[1]); } else { return null; } } /** OSGi Component Methods **/ @Activate protected void activate(ComponentContext componentContext) { configure(componentContext); } @Deactivate protected void deactivate(ComponentContext componentContext) { enabled = false; } private void configure(ComponentContext componentContext) { Dictionary properties = componentContext.getProperties(); this.enabled = PropertiesUtil.toBoolean(properties.get(PROP_ENABLED), DEFAULT_ENABLED); this.systemErrorPagePath = PropertiesUtil.toString(properties.get(PROP_ERROR_PAGE_PATH), DEFAULT_SYSTEM_ERROR_PAGE_PATH_DEFAULT); this.errorPageExtension = PropertiesUtil.toString(properties.get(PROP_ERROR_PAGE_EXTENSION), DEFAULT_ERROR_PAGE_EXTENSION); this.fallbackErrorName = PropertiesUtil.toString(properties.get(PROP_FALLBACK_ERROR_NAME), DEFAULT_FALLBACK_ERROR_NAME); this.pathMap = configurePathMap( PropertiesUtil.toStringArray(properties.get(PROP_SEARCH_PATHS), DEFAULT_SEARCH_PATHS)); } /** * Covert OSGi Property storing Root content paths:Error page paths into a SortMap * * @param paths * @return */ private SortedMap<String, String> configurePathMap(String[] paths) { SortedMap<String, String> sortedMap = new TreeMap<String, String>(new StringLengthComparator()); for (String path : paths) { if (StringUtils.isBlank(path)) { continue; } final SimpleEntry tmp = toSimpleEntry(path, ":"); if (tmp == null) { continue; } String key = StringUtils.strip((String) tmp.getKey()); String val = StringUtils.strip((String) tmp.getValue()); // Only accept absolute paths if (StringUtils.isBlank(key) || !StringUtils.startsWith(key, "/")) { continue; } // Validate page name value if (StringUtils.isBlank(val)) { val = key + "/" + DEFAULT_ERROR_PAGE_NAME; } else if (StringUtils.equals(val, ".")) { val = key; } else if (!StringUtils.startsWith(val, "/")) { val = key + "/" + val; } sortedMap.put(key, val); } return sortedMap; } }