blob: 9d7b461e6032537f0d9c7e3f8a5166f0509bf39b [file] [log] [blame]
/*
* 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.struts2.result;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.reflection.ReflectionException;
import com.opensymphony.xwork2.util.reflection.ReflectionExceptionHandler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
import org.apache.struts2.views.util.UrlHelper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static javax.servlet.http.HttpServletResponse.SC_FOUND;
/**
* Calls the {@link HttpServletResponse#sendRedirect(String) sendRedirect}
* method to the location specified. The response is told to redirect the
* browser to the specified location (a new request from the client). The
* consequence of doing this means that the action (action instance, action
* errors, field errors, etc) that was just executed is lost and no longer
* available. This is because actions are built on a single-thread model. The
* only way to pass data is through the session or with web parameters
* (url?name=value) which can be OGNL expressions.
*
* <b>This result type takes the following parameters:</b>
*
* <ul>
* <li><b>location (default)</b> - the location to go to after execution.</li>
* <li><b>parse</b> - true by default. If set to false, the location param will
* not be parsed for Ognl expressions.</li>
* <li><b>anchor</b> - Optional. Also known as "fragment" or colloquially as
* "hash". You can specify an anchor for a result.</li>
* </ul>
* This result follows the same rules from {@link StrutsResultSupport}.
*
* <b>Example:</b>
* <pre>
* <!-- START SNIPPET: example -->
* &lt;!--
* The redirect URL generated will be:
* /foo.jsp#FRAGMENT
* --&gt;
* &lt;result name="success" type="redirect"&gt;
* &lt;param name="location"&gt;foo.jsp&lt;/param&gt;
* &lt;param name="parse"&gt;false&lt;/param&gt;
* &lt;param name="anchor"&gt;FRAGMENT&lt;/param&gt;
* &lt;/result&gt;
* <!-- END SNIPPET: example -->
* </pre>
*/
public class ServletRedirectResult extends StrutsResultSupport implements ReflectionExceptionHandler, Redirectable {
private static final long serialVersionUID = 6316947346435301270L;
private static final Logger LOG = LogManager.getLogger(ServletRedirectResult.class);
protected boolean prependServletContext = true;
protected ActionMapper actionMapper;
protected int statusCode = SC_FOUND;
protected boolean suppressEmptyParameters = false;
protected Map<String, Object> requestParameters = new LinkedHashMap<>();
protected String anchor;
private UrlHelper urlHelper;
public ServletRedirectResult() {
super();
}
public ServletRedirectResult(String location) {
this(location, null);
}
public ServletRedirectResult(String location, String anchor) {
super(location);
this.anchor = anchor;
}
@Inject
public void setActionMapper(ActionMapper mapper) {
this.actionMapper = mapper;
}
@Inject
public void setUrlHelper(UrlHelper urlHelper) {
this.urlHelper = urlHelper;
}
public void setStatusCode(int code) {
this.statusCode = code;
}
/**
* Set the optional anchor value.
*
* @param anchor the anchor value
*/
public void setAnchor(String anchor) {
this.anchor = anchor;
}
/**
* Sets whether or not to prepend the servlet context path to the redirected
* URL.
*
* @param prependServletContext <tt>true</tt> to prepend the location with the servlet context path, <tt>false</tt> otherwise.
*/
public void setPrependServletContext(boolean prependServletContext) {
this.prependServletContext = prependServletContext;
}
public void execute(ActionInvocation invocation) throws Exception {
if (anchor != null) {
anchor = conditionalParse(anchor, invocation);
}
super.execute(invocation);
}
/**
* Redirects to the location specified by calling
* {@link HttpServletResponse#sendRedirect(String)}.
*
* @param finalLocation the location to redirect to.
* @param invocation an encapsulation of the action execution state.
* @throws Exception if an error occurs when redirecting.
*/
protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
ActionContext ctx = invocation.getInvocationContext();
HttpServletRequest request = ctx.getServletRequest();
HttpServletResponse response = ctx.getServletResponse();
if (isPathUrl(finalLocation)) {
if (!finalLocation.startsWith("/")) {
ActionMapping mapping = actionMapper.getMapping(request, Dispatcher.getInstance().getConfigurationManager());
String namespace = null;
if (mapping != null) {
namespace = mapping.getNamespace();
}
if ((namespace != null) && (namespace.length() > 0) && (!"/".equals(namespace))) {
finalLocation = namespace + "/" + finalLocation;
} else {
finalLocation = "/" + finalLocation;
}
}
// if the URL's are relative to the servlet context, append the servlet context path
if (prependServletContext && (request.getContextPath() != null) && (request.getContextPath().length() > 0)) {
finalLocation = request.getContextPath() + finalLocation;
}
}
ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(invocation.getResultCode());
if (resultConfig != null) {
Map<String, String> resultConfigParams = resultConfig.getParams();
List<String> prohibitedResultParams = getProhibitedResultParams();
for (Map.Entry<String, String> e : resultConfigParams.entrySet()) {
if (!prohibitedResultParams.contains(e.getKey())) {
Collection<String> values = conditionalParseCollection(e.getValue(), invocation, suppressEmptyParameters);
if (!suppressEmptyParameters || !values.isEmpty()) {
requestParameters.put(e.getKey(), values);
}
}
}
}
StringBuilder tmpLocation = new StringBuilder(finalLocation);
urlHelper.buildParametersString(requestParameters, tmpLocation, "&");
// add the anchor
if (anchor != null) {
tmpLocation.append('#').append(anchor);
}
finalLocation = response.encodeRedirectURL(tmpLocation.toString());
LOG.debug("Redirecting to finalLocation: {}", finalLocation);
sendRedirect(response, finalLocation);
}
protected List<String> getProhibitedResultParams() {
return Arrays.asList(
DEFAULT_PARAM,
"namespace",
"method",
"encode",
"parse",
"location",
"prependServletContext",
"suppressEmptyParameters",
"anchor",
"statusCode"
);
}
/**
* Sends the redirection. Can be overridden to customize how the redirect is
* handled (i.e. to use a different status code)
*
* @param response The response
* @param finalLocation The location URI
* @throws IOException in case of IO errors
*/
protected void sendRedirect(HttpServletResponse response, String finalLocation) throws IOException {
try {
if (SC_FOUND == statusCode) {
response.sendRedirect(finalLocation);
} else {
response.setStatus(statusCode);
response.setHeader("Location", finalLocation);
try {
response.getWriter().write(finalLocation);
} finally {
response.getWriter().close();
}
}
} catch (IOException ioe) {
LOG.warn("Unable to redirect to: {}, code: {}; {}", finalLocation, statusCode, ioe);
throw ioe; // Re-throw required to preserve existing default behaviour (no stacktrace in above warn for this reason)
} catch (IllegalStateException ise) {
LOG.warn("Unable to redirect to: {}, code: {}; isCommited: {}; {}", finalLocation, statusCode, response.isCommitted(), ise);
throw ise; // Re-throw required to preserve existing default behaviour (no stacktrace in above warn for this reason)
}
}
/**
* Checks if url is simple path or either full url
*
* @param url string
* @return true if it's just a path not a full url
*/
protected boolean isPathUrl(String url) {
try {
String rawUrl = url;
if (url.contains("?")) {
rawUrl = url.substring(0, url.indexOf('?'));
}
URI uri = URI.create(rawUrl.replaceAll(" ", "%20"));
if (uri.isAbsolute()) {
URL validUrl = uri.toURL();
LOG.debug("[{}] is full url, not a path", url);
return validUrl.getProtocol() == null;
} else {
LOG.debug("[{}] isn't absolute URI, assuming it's a path", url);
return true;
}
} catch (IllegalArgumentException e) {
LOG.debug("[{}] isn't a valid URL, assuming it's a path", url, e);
return true;
} catch (MalformedURLException e) {
LOG.debug("[{}] isn't a valid URL, assuming it's a path", url, e);
return true;
}
}
/**
* Sets the suppressEmptyParameters option
*
* @param suppressEmptyParameters The new value for this option
*/
public void setSuppressEmptyParameters(boolean suppressEmptyParameters) {
this.suppressEmptyParameters = suppressEmptyParameters;
}
/**
* Adds a request parameter to be added to the redirect url
*
* @param key The parameter name
* @param value The parameter value
* @return the servlet redirect result
*/
public ServletRedirectResult addParameter(String key, Object value) {
requestParameters.put(key, String.valueOf(value));
return this;
}
public void handle(ReflectionException ex) {
// Only log as debug as they are probably parameters to be appended to the url
if (LOG.isDebugEnabled()) {
LOG.debug(ex.getMessage(), ex);
}
}
}