| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.sling.servlets.get.impl; |
| |
| import java.io.IOException; |
| |
| import javax.servlet.Servlet; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.sling.api.SlingConstants; |
| import org.apache.sling.api.SlingHttpServletRequest; |
| import org.apache.sling.api.SlingHttpServletResponse; |
| import org.apache.sling.api.request.RequestPathInfo; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceUtil; |
| import org.apache.sling.api.resource.ValueMap; |
| import org.apache.sling.api.servlets.SlingSafeMethodsServlet; |
| import org.apache.sling.servlets.get.impl.helpers.JsonRendererServlet; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.metatype.annotations.AttributeDefinition; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.osgi.service.metatype.annotations.ObjectClassDefinition; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * The <code>RedirectServlet</code> implements support for GET requests to |
| * resources of type <code>sling:redirect</code>. This servlet tries to get the |
| * redirect target by |
| * <ul> |
| * <li>first adapting the resource to a {@link ValueMap} and trying to get the |
| * property <code>sling:target</code>.</li> |
| * <li>The second attempt is to access the resource <code>sling:target</code> |
| * below the requested resource and attapt this to a string.</li> |
| * </ul> |
| * <p> |
| * If there is no value found for <code>sling:target</code> a 404 (NOT FOUND) |
| * status is sent by this servlet. Otherwise a 302 (FOUND, temporary redirect) |
| * status is sent where the target is the relative URL from the current resource |
| * to the target resource. Selectors, extension, suffix and query string are |
| * also appended to the redirect URL. |
| */ |
| @SuppressWarnings("serial") |
| @Component(service = Servlet.class, |
| property = { |
| "service.description=Request Redirect Servlet", |
| "service.vendor=The Apache Software Foundation", |
| "sling.servlet.resourceTypes=sling:redirect", |
| "sling.servlet.methods=GET", |
| "sling.servlet.prefix:Integer=-1" |
| }) |
| @Designate(ocd = RedirectServlet.Config.class) |
| public class RedirectServlet extends SlingSafeMethodsServlet { |
| |
| @ObjectClassDefinition(name="Apache Sling Redirect Servlet", |
| description="The Sling servlet handling redirect resources.") |
| public @interface Config { |
| |
| @AttributeDefinition(name = "JSON Max results", |
| description = "The maximum number of resources that should " + |
| "be returned when doing a node.5.json or node.infinity.json. In JSON terms " + |
| "this basically means the number of Objects to return. Default value is " + |
| "200.") |
| int json_maximumresults() default 200; |
| } |
| |
| /** The name of the target property */ |
| public static final String TARGET_PROP = "sling:target"; |
| |
| /** The name of the redirect status property */ |
| public static final String STATUS_PROP = "sling:status"; |
| |
| /** default log */ |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| private Servlet jsonRendererServlet; |
| |
| private int jsonMaximumResults; |
| |
| @Activate |
| protected void activate(Config cfg) { |
| this.jsonMaximumResults = cfg.json_maximumresults(); |
| // When the maximumResults get updated, we force a reset for the jsonRendererServlet. |
| jsonRendererServlet = getJsonRendererServlet(); |
| } |
| |
| @Override |
| protected void doGet(SlingHttpServletRequest request, |
| SlingHttpServletResponse response) throws ServletException, |
| IOException { |
| |
| // handle json export of the redirect node |
| if (JsonRendererServlet.EXT_JSON.equals(request.getRequestPathInfo().getExtension())) { |
| getJsonRendererServlet().service(request, response); |
| return; |
| } |
| |
| // check for redirectability |
| if (response.isCommitted()) { |
| // committed response cannot be redirected |
| log.warn("RedirectServlet: Response is already committed, not redirecting"); |
| request.getRequestProgressTracker().log( |
| "RedirectServlet: Response is already committed, not redirecting"); |
| return; |
| } else if (request.getAttribute(SlingConstants.ATTR_REQUEST_SERVLET) != null) { |
| // included request will not redirect |
| log.warn("RedirectServlet: Servlet is included, not redirecting"); |
| request.getRequestProgressTracker().log( |
| "RedirectServlet: Servlet is included, not redirecting"); |
| return; |
| } |
| |
| String targetPath = null; |
| |
| // convert resource to a value map |
| final Resource rsrc = request.getResource(); |
| final ValueMap valueMap = rsrc.adaptTo(ValueMap.class); |
| if (valueMap != null) { |
| targetPath = valueMap.get(TARGET_PROP, String.class); |
| } |
| if (targetPath == null) { |
| // old behaviour |
| final Resource targetResource = request.getResourceResolver().getResource( |
| rsrc, TARGET_PROP); |
| if (targetResource == null) { |
| response.sendError(HttpServletResponse.SC_NOT_FOUND, |
| "Missing target for redirection"); |
| return; |
| } |
| |
| // if the target resource is a path (string), redirect there |
| targetPath = targetResource.adaptTo(String.class); |
| } |
| |
| // if we got a target path, make it external and redirect to it |
| if (targetPath != null) { |
| if (!isUrl(targetPath)) { |
| // make path relative and append selectors, extension etc. |
| // this is an absolute URI suitable for the Location header |
| targetPath = toRedirectPath(targetPath, request); |
| } else { |
| // just append any selectors, extension, suffix and query string |
| targetPath = appendSelectorsExtensionSuffixQuery(request, |
| new StringBuilder(targetPath)).toString(); |
| } |
| |
| final int status = getStatus(valueMap); |
| |
| // redirect the client, use our own setup since we might have a |
| // custom response status and we already have converted the target |
| // into an absolute URI. |
| response.reset(); |
| response.setStatus(status); |
| response.setHeader("Location", isUrl(targetPath) ? targetPath : response.encodeRedirectURL(targetPath)); |
| response.flushBuffer(); |
| |
| return; |
| } |
| |
| // no way of finding the target, just fail |
| response.sendError(HttpServletResponse.SC_NOT_FOUND, |
| "Cannot redirect to target resource " + targetPath); |
| } |
| |
| /** |
| * Returns the response status from the {@link #STATUS_PROP} property in the |
| * value map. If <code>valueMap</code> is <code>null</code>, the property is |
| * not contained in the map or if the value is outside of the value HTTP |
| * response status range of [ 100 .. 999 ], the default status 302/FOUND is |
| * returned. |
| * |
| * @param valueMap The <code>valueMap</code> providing the optional status |
| * property. |
| * @return The status value as defined above. |
| */ |
| static int getStatus(final ValueMap valueMap) { |
| if (valueMap != null) { |
| final Integer statusInt = valueMap.get(STATUS_PROP, Integer.class); |
| if (statusInt != null) { |
| int status = statusInt.intValue(); |
| if (status >= 100 && status <= 999) { |
| return status; |
| } |
| } |
| } |
| |
| // fall back to default value |
| return HttpServletResponse.SC_FOUND; |
| |
| } |
| |
| /** |
| * Create an absolute URI suitable for the "Location" response header |
| * including any selectors, extension, suffix and query from the current |
| * request. |
| */ |
| static String toRedirectPath(String targetPath, |
| SlingHttpServletRequest request) { |
| |
| // make sure the target path is absolute |
| final String rawAbsPath; |
| if (targetPath.startsWith("/")) { |
| rawAbsPath = targetPath; |
| } else { |
| rawAbsPath = request.getResource().getPath() + "/" + targetPath; |
| } |
| |
| final StringBuilder target = new StringBuilder(); |
| |
| // and ensure the path is normalized, us unnormalized if not possible |
| final String absPath = ResourceUtil.normalize(rawAbsPath); |
| if (absPath == null) { |
| target.append(rawAbsPath); |
| } else { |
| target.append(absPath); |
| } |
| |
| appendSelectorsExtensionSuffixQuery(request, target); |
| |
| // return the mapped full path |
| return request.getResourceResolver().map(request, target.toString()); |
| } |
| |
| /** |
| * Appends optional request selectors, extension, suffix and query string to |
| * the URL to be prepared in the target string builder and returns the |
| * string builder. |
| * |
| * @param request The Sling HTTP Servlet Request providing access to the |
| * data to be appended |
| * @param target The String builder to append the data to. This must not be |
| * null. |
| * @return The <code>target</code> string builder. |
| * @throws NullPointerException if request or target is <code>null</code>. |
| */ |
| private static StringBuilder appendSelectorsExtensionSuffixQuery( |
| SlingHttpServletRequest request, StringBuilder target) { |
| // append current selectors, extension and suffix |
| final RequestPathInfo rpi = request.getRequestPathInfo(); |
| if (rpi.getExtension() != null) { |
| |
| if (rpi.getSelectorString() != null) { |
| target.append('.').append(rpi.getSelectorString()); |
| } |
| |
| target.append('.').append(rpi.getExtension()); |
| |
| if (rpi.getSuffix() != null) { |
| target.append(rpi.getSuffix()); |
| } |
| } |
| |
| // append current querystring |
| if (request.getQueryString() != null) { |
| target.append('?').append(request.getQueryString()); |
| } |
| |
| return target; |
| } |
| |
| /** |
| * Returns an absolute URI built from the given parameters. |
| * |
| * @param scheme The scheme for the URI to be built. |
| * @param host The name of the host. |
| * @param port The port or -1 to not add a port number to the URI. For |
| * <code>http</code> and <code>https</code> schemes the port is |
| * not added if it is the default port. |
| * @param targetPath The path of the resulting URI. This path is expected to |
| * not be an absolute URI. |
| * @return The absolute URI built from the components. |
| */ |
| static String toAbsoluteUri(final String scheme, final String host, |
| final int port, final String targetPath) { |
| |
| // 1. scheme and host |
| final StringBuilder absUriBuilder = new StringBuilder(); |
| absUriBuilder.append(scheme).append("://").append(host); |
| |
| // 2. append the port depending on the scheme and whether the port is |
| // the default or not |
| if (port > 0) { |
| if (!(("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443))) { |
| absUriBuilder.append(':').append(port); |
| } |
| } |
| |
| // 3. the actual target path |
| absUriBuilder.append(targetPath); |
| return absUriBuilder.toString(); |
| } |
| |
| private Servlet getJsonRendererServlet() { |
| if (jsonRendererServlet == null) { |
| Servlet jrs = new JsonRendererServlet(jsonMaximumResults); |
| try { |
| jrs.init(getServletConfig()); |
| } catch (Exception e) { |
| // don't care too much here |
| } |
| jsonRendererServlet = jrs; |
| } |
| return jsonRendererServlet; |
| } |
| |
| /** |
| * Returns <code>true</code> if the path is potentially an URL. This |
| * checks whether the path starts with a scheme followed by a colon |
| * according to <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC-2396</a>: |
| * <pre> |
| * scheme = alpha *( alpha | digit | "+" | "-" | "." ) |
| * alpha = [ "A" .. "Z", "a" .. "z" ] |
| * digit = [ "0" .. "9" ] |
| * </pre> |
| */ |
| private static boolean isUrl(final String path) { |
| for (int i = 0; i < path.length(); i++) { |
| char c = path.charAt(i); |
| if (c == ':') { |
| return true; |
| } |
| if (!((c >= 'a' && c <= 'z') |
| || (c >= 'A' && c <= 'Z') |
| || (i > 0 |
| && ((c >= '0' && c <= '9') |
| || c == '.' |
| || c == '+' |
| || c == '-')))) { |
| break; |
| } |
| } |
| return false; |
| } |
| |
| } |