http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/interceptor/MessageStoreInterceptor.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/interceptor/MessageStoreInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/MessageStoreInterceptor.java index 82d2a55..f10cd48 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/MessageStoreInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/MessageStoreInterceptor.java @@ -27,7 +27,7 @@ import com.opensymphony.xwork2.interceptor.ValidationAware; import com.opensymphony.xwork2.interceptor.AbstractInterceptor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.struts2.dispatcher.ServletRedirectResult; +import org.apache.struts2.result.ServletRedirectResult; import java.util.ArrayList; import java.util.Collection;
http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/HttpHeaderResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/HttpHeaderResult.java b/core/src/main/java/org/apache/struts2/result/HttpHeaderResult.java new file mode 100644 index 0000000..4745475 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/HttpHeaderResult.java @@ -0,0 +1,210 @@ +/* + * $Id$ + * + * 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.Result; +import com.opensymphony.xwork2.util.TextParseUtil; +import com.opensymphony.xwork2.util.ValueStack; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ServletActionContext; + +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + + +/** + * <!-- START SNIPPET: description --> + * <p/> + * A custom Result type for setting HTTP headers and status by optionally evaluating against the ValueStack. + * This result can also be used to send and error to the client. All the parameters can be evaluated against the ValueStack. + * <p/> + * <!-- END SNIPPET: description --> + * <p/> + * <b>This result type takes the following parameters:</b> + * <p/> + * <!-- START SNIPPET: params --> + * <p/> + * <ul> + * <p/> + * <li><b>status</b> - the http servlet response status code that should be set on a response.</li> + * <p/> + * <li><b>parse</b> - true by default. If set to false, the headers param will not be parsed for Ognl expressions.</li> + * <p/> + * <li><b>headers</b> - header values.</li> + * <p/> + * <li><b>error</b> - the http servlet response error code that should be set on a response.</li> + * <p/> + * <li><b>errorMessage</b> - error message to be set on response if 'error' is set.</li> + * </ul> + * <p/> + * <!-- END SNIPPET: params --> + * <p/> + * <b>Example:</b> + * <p/> + * <pre><!-- START SNIPPET: example --> + * <result name="success" type="httpheader"> + * <param name="status">204</param> + * <param name="headers.a">a custom header value</param> + * <param name="headers.b">another custom header value</param> + * </result> + * <p/> + * <result name="proxyRequired" type="httpheader"> + * <param name="error">305</param> + * <param name="errorMessage">this action must be accessed through a prozy</param> + * </result> + * <!-- END SNIPPET: example --></pre> + */ +public class HttpHeaderResult implements Result { + + private static final long serialVersionUID = 195648957144219214L; + private static final Logger LOG = LogManager.getLogger(HttpHeaderResult.class); + + /** + * This result type doesn't have a default param, null is ok to reduce noise in logs + */ + public static final String DEFAULT_PARAM = null; + + private boolean parse = true; + private Map<String, String> headers; + private int status = -1; + private String error = null; + private String errorMessage; + + public HttpHeaderResult() { + super(); + headers = new HashMap<>(); + } + + public HttpHeaderResult(int status) { + this(); + this.status = status; + this.parse = false; + } + + /** + * Sets the http servlet error code that should be set on the response + * + * @param error the Http error code + * @see javax.servlet.http.HttpServletResponse#sendError(int) + */ + public void setError(String error) { + this.error = error; + } + + /** + * Sets the error message that should be set on the reponse + * + * @param errorMessage error message send to the client + * @see javax.servlet.http.HttpServletResponse#sendError(int, String) + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * Returns a Map of all HTTP headers. + * + * @return a Map of all HTTP headers. + */ + public Map<String, String> getHeaders() { + return headers; + } + + /** + * Sets whether or not the HTTP header values should be evaluated against the ValueStack (by default they are). + * + * @param parse <tt>true</tt> if HTTP header values should be evaluated against the ValueStack, <tt>false</tt> + * otherwise. + */ + public void setParse(boolean parse) { + this.parse = parse; + } + + /** + * Sets the http servlet response status code that should be set on a response. + * + * @param status the Http status code + * @see javax.servlet.http.HttpServletResponse#setStatus(int) + */ + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } + + /** + * Adds an HTTP header to the response + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + headers.put(name, value); + } + + /** + * Sets the optional HTTP response status code and also re-sets HTTP headers after they've + * been optionally evaluated against the ValueStack. + * + * @param invocation an encapsulation of the action execution state. + * @throws Exception if an error occurs when re-setting the headers. + */ + public void execute(ActionInvocation invocation) throws Exception { + HttpServletResponse response = ServletActionContext.getResponse(); + ValueStack stack = ActionContext.getContext().getValueStack(); + + if (status != -1) { + response.setStatus(status); + } + + if (headers != null) { + for (Map.Entry<String, String> entry : headers.entrySet()) { + String value = entry.getValue(); + String finalValue = parse ? TextParseUtil.translateVariables(value, stack) : value; + response.addHeader(entry.getKey(), finalValue); + } + } + + if (status == -1 && error != null) { + int errorCode = -1; + try { + errorCode = Integer.parseInt(parse ? TextParseUtil.translateVariables(error, stack) : error); + } catch (Exception e) { + LOG.error("Cannot parse errorCode [{}] value as Integer!", error, e); + } + if (errorCode != -1) { + if (errorMessage != null) { + String finalMessage = parse ? TextParseUtil.translateVariables(errorMessage, stack) : errorMessage; + response.sendError(errorCode, finalMessage); + } else { + response.sendError(errorCode); + } + } + } + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/PlainTextResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/PlainTextResult.java b/core/src/main/java/org/apache/struts2/result/PlainTextResult.java new file mode 100644 index 0000000..44d9302 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/PlainTextResult.java @@ -0,0 +1,183 @@ +/* + * $Id$ + * + * 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.ActionInvocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.Charset; + +/** + * <!-- START SNIPPET: description --> + * + * A result that send the content out as plain text. Useful typically when needed + * to display the raw content of a JSP or Html file for example. + * + * <!-- END SNIPPET: description --> + * + * + * <!-- START SNIPPET: params --> + * + * <ul> + * <li>location (default) = location of the file (jsp/html) to be displayed as plain text.</li> + * <li>charSet (optional) = character set to be used. This character set will be used to set the + * response type (eg. Content-Type=text/plain; charset=UTF-8) and when reading + * using a Reader. Some example of charSet would be UTF-8, ISO-8859-1 etc. + * </ul> + * + * <!-- END SNIPPET: params --> + * + * + * <pre> + * <!-- START SNIPPET: example --> + * + * <action name="displayJspRawContent" > + * <result type="plainText">/myJspFile.jsp</result> + * </action> + * + * + * <action name="displayJspRawContent" > + * <result type="plainText"> + * <param name="location">/myJspFile.jsp</param> + * <param name="charSet">UTF-8</param> + * </result> + * </action> + * + * <!-- END SNIPPET: example --> + * </pre> + * + */ +public class PlainTextResult extends StrutsResultSupport { + + public static final int BUFFER_SIZE = 1024; + + private static final Logger LOG = LogManager.getLogger(PlainTextResult.class); + + private static final long serialVersionUID = 3633371605905583950L; + + private String charSet; + + public PlainTextResult() { + super(); + } + + public PlainTextResult(String location) { + super(location); + } + + /** + * Set the character set + * + * @return The character set + */ + public String getCharSet() { + return charSet; + } + + /** + * Set the character set + * + * @param charSet The character set + */ + public void setCharSet(String charSet) { + this.charSet = charSet; + } + + /* (non-Javadoc) + * @see org.apache.struts2.result.StrutsResultSupport#doExecute(java.lang.String, com.opensymphony.xwork2.ActionInvocation) + */ + protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { + // verify charset + Charset charset = readCharset(); + + HttpServletResponse response = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE); + + applyCharset(charset, response); + applyAdditionalHeaders(response); + String location = adjustLocation(finalLocation); + + try (PrintWriter writer = response.getWriter(); + InputStream resourceAsStream = readStream(invocation, location); + InputStreamReader reader = new InputStreamReader(resourceAsStream, charset == null ? Charset.defaultCharset() : charset)) { + logWrongStream(finalLocation, resourceAsStream); + sendStream(writer, reader); + } + } + + protected InputStream readStream(ActionInvocation invocation, String location) { + ServletContext servletContext = (ServletContext) invocation.getInvocationContext().get(SERVLET_CONTEXT); + return servletContext.getResourceAsStream(location); + } + + protected void logWrongStream(String finalLocation, InputStream resourceAsStream) { + if (resourceAsStream == null) { + LOG.warn("Resource at location [{}] cannot be obtained (return null) from ServletContext !!!", finalLocation); + } + } + + protected void sendStream(PrintWriter writer, InputStreamReader reader) throws IOException { + char[] buffer = new char[BUFFER_SIZE]; + int charRead; + while((charRead = reader.read(buffer)) != -1) { + writer.write(buffer, 0, charRead); + } + } + + protected String adjustLocation(String location) { + if (location.charAt(0) != '/') { + return "/" + location; + } + return location; + } + + protected void applyAdditionalHeaders(HttpServletResponse response) { + response.setHeader("Content-Disposition", "inline"); + } + + protected void applyCharset(Charset charset, HttpServletResponse response) { + if (charset != null) { + response.setContentType("text/plain; charset=" + charSet); + } else { + response.setContentType("text/plain"); + } + } + + protected Charset readCharset() { + Charset charset = null; + if (charSet != null) { + if (Charset.isSupported(charSet)) { + charset = Charset.forName(charSet); + } else { + LOG.warn("charset [{}] is not recognized", charset); + charset = null; + } + } + return charset; + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/PostbackResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/PostbackResult.java b/core/src/main/java/org/apache/struts2/result/PostbackResult.java new file mode 100644 index 0000000..ee14384 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/PostbackResult.java @@ -0,0 +1,232 @@ +/* + * $Id$ + * + * 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.inject.Inject; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.dispatcher.mapper.ActionMapper; +import org.apache.struts2.dispatcher.mapper.ActionMapping; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +/** + * <!-- START SNIPPET: description --> + * A result that renders the current request parameters as a form which + * immediately submits a <a href="http://en.wikipedia.org/wiki/Postback">postback</a> + * to the specified destination. + * <!-- END SNIPPET: description --> + * <p/> + * <b>Parameters:</b> + * <!-- START SNIPPET: params --> + * <ul> + * <li>location - http location to post the form</li> + * <li>prependServletContext (true|false) - when location is relative, controls if to add Servlet Context, default "true"</li> + * <li>actionName - action name to post the form (resolved as an expression)</li> + * <li>namespace - action's namespace to use (resolved as an expression)</li> + * <li>method - actions' method to use (resolved as an expression)</li> + * <li>cache (true|false) - when set to true adds cache control headers, default "true"</li> + * <li>parse (true|false) - when set to true actionName, namespace and method are parsed, default "true"</li> + * </ul> + * <!-- END SNIPPET: params --> + * <p/> + * <b>Examples:</b> + * <pre> + * <!-- START SNIPPET: example --> + * <action name="registerThirdParty" > + * <result type="postback">https://www.example.com/register</result> + * </action> + * + * <action name="registerThirdParty" > + * <result type="postback"> + * <param name="namespace">/secure</param> + * <param name="actionName">register2</param> + * </result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + */ +public class PostbackResult extends StrutsResultSupport { + + private String actionName; + private String namespace; + private String method; + private boolean prependServletContext = true; + private boolean cache = true; + + protected ActionMapper actionMapper; + + @Override + protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { + ActionContext ctx = invocation.getInvocationContext(); + HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST); + HttpServletResponse response = (HttpServletResponse) ctx.get(ServletActionContext.HTTP_RESPONSE); + + // Cache? + if (!cache) { + response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1 + response.setHeader("Pragma", "no-cache"); // HTTP 1.0 + response.setDateHeader("Expires", 0); // Proxies + } + + // Render + PrintWriter pw = new PrintWriter(response.getOutputStream()); + pw.write("<!DOCTYPE html><html><body><form action=\"" + finalLocation + "\" method=\"POST\">"); + writeFormElements(request, pw); + writePrologueScript(pw); + pw.write("</html>"); + pw.flush(); + } + + @Override + public void execute(ActionInvocation invocation) throws Exception { + String postbackUri = makePostbackUri(invocation); + setLocation(postbackUri); + super.execute(invocation); + } + + /** + * Determines if the specified form input element should be included. + * + * @param name the input element name + * @param values the input element values + * @return {@code true} if included; otherwise {@code false} + */ + protected boolean isElementIncluded(String name, String[] values) { + return !name.startsWith("action:"); + } + + protected String makePostbackUri(ActionInvocation invocation) { + ActionContext ctx = invocation.getInvocationContext(); + HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST); + String postbackUri; + + if (actionName != null) { + actionName = conditionalParse(actionName, invocation); + if (namespace == null) { + namespace = invocation.getProxy().getNamespace(); + } else { + namespace = conditionalParse(namespace, invocation); + } + if (method == null) { + method = ""; + } else { + method = conditionalParse(method, invocation); + } + postbackUri = request.getContextPath() + actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null)); + } else { + String location = getLocation(); + // Do not prepend if the URL is a FQN + if (!location.matches("^([a-zA-z]+:)?//.*")) { + // If the URL is relative to the servlet context, prepend the servlet context path + if (prependServletContext && (request.getContextPath() != null) && (request.getContextPath().length() > 0)) { + location = request.getContextPath() + location; + } + } + postbackUri = location; + } + + return postbackUri; + } + + @Inject + public final void setActionMapper(ActionMapper mapper) { + this.actionMapper = mapper; + } + + /** + * Sets the name of the destination action. + * + * @param actionName the action name + */ + public final void setActionName(String actionName) { + this.actionName = actionName; + } + + /** + * Stores the option to cache the rendered intermediate page. The default + * is {@code true}. + * + * @return {@code true} to cache; otherwise {@code false} + */ + public final void setCache(boolean cache) { + this.cache = cache; + } + + /** + * Sets the method of the destination action. + * + * @param method the method + */ + public final void setMethod(String method) { + this.method = method; + } + + /** + * Sets the namespace of the destination action. + * + * @param namespace the namespace + */ + public final void setNamespace(String namespace) { + this.namespace = namespace; + } + + public final void setPrependServletContext(boolean prependServletContext) { + this.prependServletContext = prependServletContext; + } + + protected void writeFormElement(PrintWriter pw, String name, String[] values) throws UnsupportedEncodingException { + for (String value : values) { + String encName = URLEncoder.encode(name, "UTF-8"); + String encValue = URLEncoder.encode(value, "UTF-8"); + pw.write("<input type=\"hidden\" name=\"" + encName + "\" value=\"" + encValue + "\"/>"); + } + } + + private void writeFormElements(HttpServletRequest request, PrintWriter pw) throws UnsupportedEncodingException { + Map<String, String[]> params = request.getParameterMap(); + for (String name : params.keySet()) { + String[] values = params.get(name); + if (isElementIncluded(name, values)) { + writeFormElement(pw, name, values); + } + } + } + + /** + * Outputs the script after the form has been emitted. The default script + * is to submit the form using a JavaScript time out that immediately expires. + * + * @param pw the print writer + */ + protected void writePrologueScript(PrintWriter pw) { + pw.write("<script>"); + pw.write("setTimeout(function(){document.forms[0].submit();},0);"); + pw.write("</script>"); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java b/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java new file mode 100644 index 0000000..2d12606 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java @@ -0,0 +1,227 @@ +/* + * $Id$ + * + * 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.ActionInvocation; +import com.opensymphony.xwork2.util.reflection.ReflectionExceptionHandler; +import org.apache.struts2.dispatcher.mapper.ActionMapper; +import org.apache.struts2.dispatcher.mapper.ActionMapping; + +import java.util.Arrays; +import java.util.List; + +/** + * <!-- START SNIPPET: description --> + * + * This result uses the {@link ActionMapper} provided by the + * {@link ActionMapperFactory} to redirect the browser to a URL that invokes the + * specified action and (optional) namespace. This is better than the + * {@link ServletRedirectResult} because it does not require you to encode the + * URL patterns processed by the {@link ActionMapper} in to your struts.xml + * configuration files. This means you can change your URL patterns at any point + * and your application will still work. It is strongly recommended that if you + * are redirecting to another action, you use this result rather than the + * standard redirect result. + * + * See examples below for an example of how request parameters could be passed + * in. + * + * <!-- END SNIPPET: description --> + * + * <b>This result type takes the following parameters:</b> + * + * <!-- START SNIPPET: params --> + * + * <ul> + * + * <li><b>actionName (default)</b> - The name of the action that will be + * redirected to.</li> + * + * <li><b>namespace</b> - Used to determine which namespace the action is in + * that we're redirecting to. If namespace is null, the default will be the + * current namespace.</li> + * + * <li><b>suppressEmptyParameters</b> - Optional boolean (defaults to false) that + * can prevent parameters with no values from being included in the redirect + * URL.</li> + * + * <li><b>parse</b> - Boolean, true by default. If set to false, the actionName + * 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> + * + * <!-- END SNIPPET: params --> + * + * <b>Example:</b> + * + * <pre> + * <!-- START SNIPPET: example --> + * <package name="public" extends="struts-default"> + * <action name="login" class="..."> + * <!-- Redirect to another namespace --> + * <result type="redirectAction"> + * <param name="actionName">dashboard</param> + * <param name="namespace">/secure</param> + * </result> + * </action> + * </package> + * + * <package name="secure" extends="struts-default" namespace="/secure"> + * <-- Redirect to an action in the same namespace --> + * <action name="dashboard" class="..."> + * <result>dashboard.jsp</result> + * <result name="error" type="redirectAction">error</result> + * </action> + * + * <action name="error" class="..."> + * <result>error.jsp</result> + * </action> + * </package> + * + * <package name="passingRequestParameters" extends="struts-default" namespace="/passingRequestParameters"> + * <!-- Pass parameters (reportType, width and height) --> + * <!-- + * The redirectAction url generated will be : + * /genReport/generateReport.action?reportType=pie&width=100&height=100#summary + * --> + * <action name="gatherReportInfo" class="..."> + * <result name="showReportResult" type="redirectAction"> + * <param name="actionName">generateReport</param> + * <param name="namespace">/genReport</param> + * <param name="reportType">pie</param> + * <param name="width">100</param> + * <param name="height">100</param> + * <param name="empty"></param> + * <param name="suppressEmptyParameters">true</param> + * <param name="anchor">summary</param> + * </result> + * </action> + * </package> + * + * + * <!-- END SNIPPET: example --> + * </pre> + * + * @see ActionMapper + */ +public class ServletActionRedirectResult extends ServletRedirectResult implements ReflectionExceptionHandler { + + private static final long serialVersionUID = -9042425229314584066L; + + /* The default parameter */ + public static final String DEFAULT_PARAM = "actionName"; + + protected String actionName; + protected String namespace; + protected String method; + + public ServletActionRedirectResult() { + super(); + } + + public ServletActionRedirectResult(String actionName) { + this(null, actionName, null, null); + } + + public ServletActionRedirectResult(String actionName, String method) { + this(null, actionName, method, null); + } + + public ServletActionRedirectResult(String namespace, String actionName, String method) { + this(namespace, actionName, method, null); + } + + public ServletActionRedirectResult(String namespace, String actionName, String method, String anchor) { + super(null, anchor); + this.namespace = namespace; + this.actionName = actionName; + this.method = method; + } + + /** + * @see com.opensymphony.xwork2.Result#execute(com.opensymphony.xwork2.ActionInvocation) + */ + public void execute(ActionInvocation invocation) throws Exception { + actionName = conditionalParse(actionName, invocation); + if (namespace == null) { + namespace = invocation.getProxy().getNamespace(); + } else { + namespace = conditionalParse(namespace, invocation); + } + if (method == null) { + method = ""; + } else { + method = conditionalParse(method, invocation); + } + + String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null)); + + setLocation(tmpLocation); + + super.execute(invocation); + } + + /** + * Sets the action name + * + * @param actionName The name + */ + public void setActionName(String actionName) { + this.actionName = actionName; + } + + /** + * Sets the namespace + * + * @param namespace The namespace + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Sets the method + * + * @param method The method + */ + public void setMethod(String method) { + this.method = method; + } + + protected List<String> getProhibitedResultParams() { + return Arrays.asList( + DEFAULT_PARAM, + "namespace", + "method", + "encode", + "parse", + "location", + "prependServletContext", + "suppressEmptyParameters", + "anchor", + "statusCode" + ); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java b/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java new file mode 100644 index 0000000..8d20e99 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/ServletDispatcherResult.java @@ -0,0 +1,174 @@ +/* + * $Id$ + * + * 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.ActionInvocation; +import com.opensymphony.xwork2.inject.Inject; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsStatics; +import org.apache.struts2.views.util.UrlHelper; + +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.PageContext; +import java.util.Map; + + +/** + * <!-- START SNIPPET: description --> + * + * Includes or forwards to a view (usually a jsp). Behind the scenes Struts + * will use a RequestDispatcher, where the target servlet/JSP receives the same + * request/response objects as the original servlet/JSP. Therefore, you can pass + * data between them using request.setAttribute() - the Struts action is + * available. + * <p/> + * There are three possible ways the result can be executed: + * + * <ul> + * + * <li>If we are in the scope of a JSP (a PageContext is available), PageContext's + * {@link PageContext#include(String) include} method is called.</li> + * + * <li>If there is no PageContext and we're not in any sort of include (there is no + * "javax.servlet.include.servlet_path" in the request attributes), then a call to + * {@link RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse) forward} + * is made.</li> + * + * <li>Otherwise, {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse) include} + * is called.</li> + * + * </ul> + * <!-- END SNIPPET: description --> + * + * <b>This result type takes the following parameters:</b> + * + * <!-- START SNIPPET: params --> + * + * <ul> + * + * <li><b>location (default)</b> - the location to go to after execution (ex. jsp).</li> + * + * <li><b>parse</b> - true by default. If set to false, the location param will not be parsed for Ognl expressions.</li> + * + * </ul> + * + * <!-- END SNIPPET: params --> + * + * <b>Example:</b> + * + * <pre><!-- START SNIPPET: example --> + * <result name="success" type="dispatcher"> + * <param name="location">foo.jsp</param> + * </result> + * <!-- END SNIPPET: example --></pre> + * + * This result follows the same rules from {@link StrutsResultSupport}. + * + * @see javax.servlet.RequestDispatcher + */ +public class ServletDispatcherResult extends StrutsResultSupport { + + private static final long serialVersionUID = -1970659272360685627L; + + private static final Logger LOG = LogManager.getLogger(ServletDispatcherResult.class); + + private UrlHelper urlHelper; + + public ServletDispatcherResult() { + super(); + } + + public ServletDispatcherResult(String location) { + super(location); + } + + @Inject + public void setUrlHelper(UrlHelper urlHelper) { + this.urlHelper = urlHelper; + } + + /** + * Dispatches to the given location. Does its forward via a RequestDispatcher. If the + * dispatch fails a 404 error will be sent back in the http response. + * + * @param finalLocation the location to dispatch to. + * @param invocation the execution state of the action + * @throws Exception if an error occurs. If the dispatch fails the error will go back via the + * HTTP request. + */ + public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { + LOG.debug("Forwarding to location: {}", finalLocation); + + PageContext pageContext = ServletActionContext.getPageContext(); + + if (pageContext != null) { + pageContext.include(finalLocation); + } else { + HttpServletRequest request = ServletActionContext.getRequest(); + HttpServletResponse response = ServletActionContext.getResponse(); + RequestDispatcher dispatcher = request.getRequestDispatcher(finalLocation); + + //add parameters passed on the location to #parameters + // see WW-2120 + if (StringUtils.isNotEmpty(finalLocation) && finalLocation.indexOf("?") > 0) { + String queryString = finalLocation.substring(finalLocation.indexOf("?") + 1); + Map<String, Object> parameters = getParameters(invocation); + Map<String, Object> queryParams = urlHelper.parseQueryString(queryString, true); + if (queryParams != null && !queryParams.isEmpty()) + parameters.putAll(queryParams); + } + + // if the view doesn't exist, let's do a 404 + if (dispatcher == null) { + response.sendError(404, "result '" + finalLocation + "' not found"); + return; + } + + //if we are inside an action tag, we always need to do an include + Boolean insideActionTag = (Boolean) ObjectUtils.defaultIfNull(request.getAttribute(StrutsStatics.STRUTS_ACTION_TAG_INVOCATION), Boolean.FALSE); + + // If we're included, then include the view + // Otherwise do forward + // This allow the page to, for example, set content type + if (!insideActionTag && !response.isCommitted() && (request.getAttribute("javax.servlet.include.servlet_path") == null)) { + request.setAttribute("struts.view_uri", finalLocation); + request.setAttribute("struts.request_uri", request.getRequestURI()); + + dispatcher.forward(request, response); + } else { + dispatcher.include(request, response); + } + } + } + + @SuppressWarnings("unchecked") + private Map<String, Object> getParameters(ActionInvocation invocation) { + return (Map<String, Object>) invocation.getInvocationContext().getContextMap().get("parameters"); + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java b/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java new file mode 100644 index 0000000..c3fe70b --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/ServletRedirectResult.java @@ -0,0 +1,323 @@ +/* + * $Id$ + * + * 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.ServletActionContext; +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.*; + +import static javax.servlet.http.HttpServletResponse.SC_FOUND; + +/** + * <!-- START SNIPPET: description --> + * + * 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. + * + * <!-- END SNIPPET: description --> + * <p/> + * <b>This result type takes the following parameters:</b> + * + * <!-- START SNIPPET: params --> + * + * <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> + * + * <p> + * This result follows the same rules from {@link StrutsResultSupport}. + * </p> + * + * <!-- END SNIPPET: params --> + * + * <b>Example:</b> + * + * <pre> + * <!-- START SNIPPET: example --> + * <!-- + * The redirect URL generated will be: + * /foo.jsp#FRAGMENT + * --> + * <result name="success" type="redirect"> + * <param name="location">foo.jsp</param> + * <param name="parse">false</param> + * <param name="anchor">FRAGMENT</param> + * </result> + * <!-- END SNIPPET: example --> + * </pre> + * + */ +public class ServletRedirectResult extends StrutsResultSupport implements ReflectionExceptionHandler { + + 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 + */ + 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 = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST); + HttpServletResponse response = (HttpServletResponse) ctx.get(ServletActionContext.HTTP_RESPONSE); + + 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 + */ + protected void sendRedirect(HttpServletResponse response, String finalLocation) throws IOException { + if (SC_FOUND == statusCode) { + response.sendRedirect(finalLocation); + } else { + response.setStatus(statusCode); + response.setHeader("Location", finalLocation); + response.getWriter().write(finalLocation); + response.getWriter().close(); + } + + } + + /** + * 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 + */ + 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); + } + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/StreamResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/StreamResult.java b/core/src/main/java/org/apache/struts2/result/StreamResult.java new file mode 100644 index 0000000..5574cc1 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/StreamResult.java @@ -0,0 +1,329 @@ +/* + * $Id$ + * + * 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.ActionInvocation; +import com.opensymphony.xwork2.util.ValueStack; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * <!-- START SNIPPET: description --> + * + * A custom Result type for sending raw data (via an InputStream) directly to the + * HttpServletResponse. Very useful for allowing users to download content. + * + * <!-- END SNIPPET: description --> + * <p/> + * <b>This result type takes the following parameters:</b> + * + * <!-- START SNIPPET: params --> + * + * <ul> + * + * <li><b>contentType</b> - the stream mime-type as sent to the web browser + * (default = <code>text/plain</code>).</li> + * + * <li><b>contentLength</b> - the stream length in bytes (the browser displays a + * progress bar).</li> + * + * <li><b>contentDisposition</b> - the content disposition header value for + * specifing the file name (default = <code>inline</code>, values are typically + * <i>attachment;filename="document.pdf"</i>.</li> + * + * <li><b>inputName</b> - the name of the InputStream property from the chained + * action (default = <code>inputStream</code>).</li> + * + * <li><b>bufferSize</b> - the size of the buffer to copy from input to output + * (default = <code>1024</code>).</li> + * + * <li><b>allowCaching</b> if set to 'false' it will set the headers 'Pragma' and 'Cache-Control' + * to 'no-cahce', and prevent client from caching the content. (default = <code>true</code>) + * + * <li><b>contentCharSet</b> if set to a string, ';charset=value' will be added to the + * content-type header, where value is the string set. If set to an expression, the result + * of evaluating the expression will be used. If not set, then no charset will be set on + * the header</li> + * </ul> + * + * <p>These parameters can also be set by exposing a similarly named getter method on your Action. For example, you can + * provide <code>getContentType()</code> to override that parameter for the current action.</p> + * + * <!-- END SNIPPET: params --> + * + * <b>Example:</b> + * + * <pre><!-- START SNIPPET: example --> + * <result name="success" type="stream"> + * <param name="contentType">image/jpeg</param> + * <param name="inputName">imageStream</param> + * <param name="contentDisposition">attachment;filename="document.pdf"</param> + * <param name="bufferSize">1024</param> + * </result> + * <!-- END SNIPPET: example --></pre> + * + */ +public class StreamResult extends StrutsResultSupport { + + private static final long serialVersionUID = -1468409635999059850L; + + protected static final Logger LOG = LogManager.getLogger(StreamResult.class); + + public static final String DEFAULT_PARAM = "inputName"; + + protected String contentType = "text/plain"; + protected String contentLength; + protected String contentDisposition = "inline"; + protected String contentCharSet ; + protected String inputName = "inputStream"; + protected InputStream inputStream; + protected int bufferSize = 1024; + protected boolean allowCaching = true; + + public StreamResult() { + super(); + } + + public StreamResult(InputStream in) { + this.inputStream = in; + } + + /** + * @return Returns the whether or not the client should be requested to allow caching of the data stream. + */ + public boolean getAllowCaching() { + return allowCaching; + } + + /** + * Set allowCaching to <tt>false</tt> to indicate that the client should be requested not to cache the data stream. + * This is set to <tt>false</tt> by default + * + * @param allowCaching Enable caching. + */ + public void setAllowCaching(boolean allowCaching) { + this.allowCaching = allowCaching; + } + + + /** + * @return Returns the bufferSize. + */ + public int getBufferSize() { + return (bufferSize); + } + + /** + * @param bufferSize The bufferSize to set. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /** + * @return Returns the contentType. + */ + public String getContentType() { + return (contentType); + } + + /** + * @param contentType The contentType to set. + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * @return Returns the contentLength. + */ + public String getContentLength() { + return contentLength; + } + + /** + * @param contentLength The contentLength to set. + */ + public void setContentLength(String contentLength) { + this.contentLength = contentLength; + } + + /** + * @return Returns the Content-disposition header value. + */ + public String getContentDisposition() { + return contentDisposition; + } + + /** + * @param contentDisposition the Content-disposition header value to use. + */ + public void setContentDisposition(String contentDisposition) { + this.contentDisposition = contentDisposition; + } + + /** + * @return Returns the charset specified by the user + */ + public String getContentCharSet() { + return contentCharSet; + } + + /** + * @param contentCharSet the charset to use on the header when sending the stream + */ + public void setContentCharSet(String contentCharSet) { + this.contentCharSet = contentCharSet; + } + + /** + * @return Returns the inputName. + */ + public String getInputName() { + return (inputName); + } + + /** + * @param inputName The inputName to set. + */ + public void setInputName(String inputName) { + this.inputName = inputName; + } + + /** + * @see StrutsResultSupport#doExecute(java.lang.String, com.opensymphony.xwork2.ActionInvocation) + */ + protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { + + // Override any parameters using values on the stack + resolveParamsFromStack(invocation.getStack(), invocation); + + // Find the Response in context + HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE); + try (OutputStream oOutput = oResponse.getOutputStream()) { + if (inputStream == null) { + // Find the inputstream from the invocation variable stack + inputStream = (InputStream) invocation.getStack().findValue(conditionalParse(inputName, invocation)); + } + + if (inputStream == null) { + String msg = ("Can not find a java.io.InputStream with the name [" + inputName + "] in the invocation stack. " + + "Check the <param name=\"inputName\"> tag specified for this action."); + LOG.error(msg); + throw new IllegalArgumentException(msg); + } + + // Set the content type + if (contentCharSet != null && ! contentCharSet.equals("")) { + oResponse.setContentType(conditionalParse(contentType, invocation)+";charset="+contentCharSet); + } + else { + oResponse.setContentType(conditionalParse(contentType, invocation)); + } + + // Set the content length + if (contentLength != null) { + String _contentLength = conditionalParse(contentLength, invocation); + int _contentLengthAsInt = -1; + try { + _contentLengthAsInt = Integer.parseInt(_contentLength); + if (_contentLengthAsInt >= 0) { + oResponse.setContentLength(_contentLengthAsInt); + } + } + catch(NumberFormatException e) { + LOG.warn("failed to recognize {} as a number, contentLength header will not be set", _contentLength, e); + } + } + + // Set the content-disposition + if (contentDisposition != null) { + oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation)); + } + + // Set the cache control headers if neccessary + if (!allowCaching) { + oResponse.addHeader("Pragma", "no-cache"); + oResponse.addHeader("Cache-Control", "no-cache"); + } + + LOG.debug("Streaming result [{}] type=[{}] length=[{}] content-disposition=[{}] charset=[{}]", + inputName, contentType, contentLength, contentDisposition, contentCharSet); + + // Copy input to output + LOG.debug("Streaming to output buffer +++ START +++"); + byte[] oBuff = new byte[bufferSize]; + int iSize; + while (-1 != (iSize = inputStream.read(oBuff))) { + oOutput.write(oBuff, 0, iSize); + } + LOG.debug("Streaming to output buffer +++ END +++"); + + // Flush + oOutput.flush(); + } + } + + /** + * Tries to lookup the parameters on the stack. Will override any existing parameters + * + * @param stack The current value stack + */ + protected void resolveParamsFromStack(ValueStack stack, ActionInvocation invocation) { + String disposition = stack.findString("contentDisposition"); + if (disposition != null) { + setContentDisposition(disposition); + } + + String contentType = stack.findString("contentType"); + if (contentType != null) { + setContentType(contentType); + } + + String inputName = stack.findString("inputName"); + if (inputName != null) { + setInputName(inputName); + } + + String contentLength = stack.findString("contentLength"); + if (contentLength != null) { + setContentLength(contentLength); + } + + Integer bufferSize = (Integer) stack.findValue("bufferSize", Integer.class); + if (bufferSize != null) { + setBufferSize(bufferSize); + } + + if (contentCharSet != null ) { + contentCharSet = conditionalParse(contentCharSet, invocation); + } + else { + contentCharSet = stack.findString("contentCharSet"); + } + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/StrutsResultSupport.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/StrutsResultSupport.java b/core/src/main/java/org/apache/struts2/result/StrutsResultSupport.java new file mode 100644 index 0000000..bdb1b93 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/StrutsResultSupport.java @@ -0,0 +1,266 @@ +/* + * $Id$ + * + * 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.ActionInvocation; +import com.opensymphony.xwork2.Result; +import com.opensymphony.xwork2.util.TextParseUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.StrutsStatics; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; + + +/** + * <!-- START SNIPPET: javadoc --> + * + * A base class for all Struts action execution results. + * The "location" param is the default parameter, meaning the most common usage of this result would be: + * <p/> + * This class provides two common parameters for any subclass: + * <ul> + * <li>location - the location to go to after execution (could be a jsp page or another action). + * It can be parsed as per the rules definied in the + * {@link TextParseUtil#translateVariables(java.lang.String, com.opensymphony.xwork2.util.ValueStack) translateVariables} + * method</li> + * <li>parse - true by default. If set to false, the location param will not be parsed for expressions</li> + * <li>encode - false by default. If set to false, the location param will not be url encoded. This only have effect when parse is true</li> + * </ul> + * + * <b>NOTE:</b> + * The encode param will only have effect when parse is true + * + * <!-- END SNIPPET: javadoc --> + * + * <p/> + * + * <!-- START SNIPPET: example --> + * + * <p/> + * In the struts.xml configuration file, these would be included as: + * <p/> + * <pre> + * <result name="success" type="redirect"> + * <param name="<b>location</b>">foo.jsp</param> + * </result></pre> + * <p/> + * or + * <p/> + * <pre> + * <result name="success" type="redirect" > + * <param name="<b>location</b>">foo.jsp?url=${myUrl}</param> + * <param name="<b>parse</b>">true</param> + * <param name="<b>encode</b>">true</param> + * </result></pre> + * <p/> + * In the above case, myUrl will be parsed against Ognl Value Stack and then + * URL encoded. + * <p/> + * or when using the default parameter feature + * <p/> + * <pre> + * <result name="success" type="redirect"><b>foo.jsp</b></result></pre> + * <p/> + * You should subclass this class if you're interested in adding more parameters or functionality + * to your Result. If you do subclass this class you will need to + * override {@link #doExecute(String, ActionInvocation)}.<p> + * <p/> + * Any custom result can be defined in struts.xml as: + * <p/> + * <pre> + * <result-types> + * ... + * <result-type name="myresult" class="com.foo.MyResult" /> + * </result-types></pre> + * <p/> + * Please see the {@link com.opensymphony.xwork2.Result} class for more info on Results in general. + * + * <!-- END SNIPPET: example --> + * + * @see com.opensymphony.xwork2.Result + */ +public abstract class StrutsResultSupport implements Result, StrutsStatics { + + private static final Logger LOG = LogManager.getLogger(StrutsResultSupport.class); + + /** The default parameter */ + public static final String DEFAULT_PARAM = "location"; + + /** use UTF-8 as this is the recommended encoding by W3C to avoid incompatibilities. */ + public static final String DEFAULT_URL_ENCODING = "UTF-8"; + + private boolean parse; + private boolean encode; + private String location; + private String lastFinalLocation; + + public StrutsResultSupport() { + this(null, true, false); + } + + public StrutsResultSupport(String location) { + this(location, true, false); + } + + public StrutsResultSupport(String location, boolean parse, boolean encode) { + this.location = location; + this.parse = parse; + this.encode = encode; + } + + /** + * The location to go to after action execution. This could be a JSP page or another action. + * The location can contain OGNL expressions which will be evaulated if the <tt>parse</tt> + * parameter is set to <tt>true</tt>. + * + * @param location the location to go to after action execution. + * @see #setParse(boolean) + */ + public void setLocation(String location) { + this.location = location; + } + + /** + * Gets the location it was created with, mainly for testing + */ + public String getLocation() { + return location; + } + + /** + * Returns the last parsed and encoded location value + */ + public String getLastFinalLocation() { + return lastFinalLocation; + } + + /** + * Set parse to <tt>true</tt> to indicate that the location should be parsed as an OGNL expression. This + * is set to <tt>true</tt> by default. + * + * @param parse <tt>true</tt> if the location parameter is an OGNL expression, <tt>false</tt> otherwise. + */ + public void setParse(boolean parse) { + this.parse = parse; + } + + /** + * Set encode to <tt>true</tt> to indicate that the location should be url encoded. This is set to + * <tt>true</tt> by default + * + * @param encode <tt>true</tt> if the location parameter should be url encode, <tt>false</tt> otherwise. + */ + public void setEncode(boolean encode) { + this.encode = encode; + } + + /** + * Implementation of the <tt>execute</tt> method from the <tt>Result</tt> interface. This will call + * the abstract method {@link #doExecute(String, ActionInvocation)} after optionally evaluating the + * location as an OGNL evaluation. + * + * @param invocation the execution state of the action. + * @throws Exception if an error occurs while executing the result. + */ + public void execute(ActionInvocation invocation) throws Exception { + lastFinalLocation = conditionalParse(location, invocation); + doExecute(lastFinalLocation, invocation); + } + + /** + * Parses the parameter for OGNL expressions against the valuestack + * + * @param param The parameter value + * @param invocation The action invocation instance + * @return The resulting string + */ + protected String conditionalParse(String param, ActionInvocation invocation) { + if (parse && param != null && invocation != null) { + return TextParseUtil.translateVariables( + param, + invocation.getStack(), + new EncodingParsedValueEvaluator()); + } else { + return param; + } + } + + /** + * As {@link #conditionalParse(String, ActionInvocation)} but does not + * convert found object into String. If found object is a collection it is + * returned if found object is not a collection it is wrapped in one. + * + * @param param + * @param invocation + * @param excludeEmptyElements + * @return + */ + protected Collection<String> conditionalParseCollection(String param, ActionInvocation invocation, boolean excludeEmptyElements) { + if (parse && param != null && invocation != null) { + return TextParseUtil.translateVariablesCollection( + param, + invocation.getStack(), + excludeEmptyElements, + new EncodingParsedValueEvaluator()); + } else { + Collection<String> collection = new ArrayList<>(1); + collection.add(param); + return collection; + } + } + + /** + * {@link com.opensymphony.xwork2.util.TextParseUtil.ParsedValueEvaluator} to do URL encoding for found values. To be + * used for single strings or collections. + * + */ + private final class EncodingParsedValueEvaluator implements TextParseUtil.ParsedValueEvaluator { + public Object evaluate(String parsedValue) { + if (encode) { + if (parsedValue != null) { + try { + return URLEncoder.encode(parsedValue, DEFAULT_URL_ENCODING); + } + catch(UnsupportedEncodingException e) { + LOG.warn("error while trying to encode [{}]", parsedValue, e); + } + } + } + return parsedValue; + } + } + + /** + * Executes the result given a final location (jsp page, action, etc) and the action invocation + * (the state in which the action was executed). Subclasses must implement this class to handle + * custom logic for result handling. + * + * @param finalLocation the location (jsp page, action, etc) to go to. + * @param invocation the execution state of the action. + * @throws Exception if an error occurs while executing the result. + */ + protected abstract void doExecute(String finalLocation, ActionInvocation invocation) throws Exception; +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/result/VelocityResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/result/VelocityResult.java b/core/src/main/java/org/apache/struts2/result/VelocityResult.java new file mode 100644 index 0000000..810d191 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/result/VelocityResult.java @@ -0,0 +1,233 @@ +/* + * $Id$ + * + * 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.inject.Inject; +import com.opensymphony.xwork2.util.ValueStack; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.views.JspSupportServlet; +import org.apache.struts2.views.velocity.VelocityManager; +import org.apache.velocity.Template; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.context.Context; + +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.jsp.JspFactory; +import javax.servlet.jsp.PageContext; +import java.io.OutputStreamWriter; +import java.io.Writer; + + +/** + * <!-- START SNIPPET: description --> + * + * Using the Servlet container's {@link JspFactory}, this result mocks a JSP + * execution environment and then displays a Velocity template that will be + * streamed directly to the servlet output. + * + * <!-- END SNIPPET: description --> + * <p/> + * <b>This result type takes the following parameters:</b> + * + * <!-- START SNIPPET: params --> + * + * <ul> + * + * <li><b>location (default)</b> - the location of the template to process.</li> + * + * <li><b>parse</b> - true by default. If set to false, the location param will + * not be parsed for Ognl expressions.</li> + * + * </ul> + * <p> + * This result follows the same rules from {@link StrutsResultSupport}. + * </p> + * + * <!-- END SNIPPET: params --> + * + * <b>Example:</b> + * + * <pre><!-- START SNIPPET: example --> + * <result name="success" type="velocity"> + * <param name="location">foo.vm</param> + * </result> + * <!-- END SNIPPET: example --></pre> + * + */ +public class VelocityResult extends StrutsResultSupport { + + private static final long serialVersionUID = 7268830767762559424L; + + private static final Logger LOG = LogManager.getLogger(VelocityResult.class); + + private String defaultEncoding; + private VelocityManager velocityManager; + private JspFactory jspFactory = JspFactory.getDefaultFactory(); + + public VelocityResult() { + super(); + } + + public VelocityResult(String location) { + super(location); + } + + @Inject(StrutsConstants.STRUTS_I18N_ENCODING) + public void setDefaultEncoding(String val) { + defaultEncoding = val; + } + + @Inject + public void setVelocityManager(VelocityManager mgr) { + this.velocityManager = mgr; + } + + /** + * Creates a Velocity context from the action, loads a Velocity template and executes the + * template. Output is written to the servlet output stream. + * + * @param finalLocation the location of the Velocity template + * @param invocation an encapsulation of the action execution state. + * @throws Exception if an error occurs when creating the Velocity context, loading or executing + * the template or writing output to the servlet response stream. + */ + public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { + ValueStack stack = ActionContext.getContext().getValueStack(); + + HttpServletRequest request = ServletActionContext.getRequest(); + HttpServletResponse response = ServletActionContext.getResponse(); + ServletContext servletContext = ServletActionContext.getServletContext(); + Servlet servlet = JspSupportServlet.jspSupportServlet; + + velocityManager.init(servletContext); + + boolean usedJspFactory = false; + PageContext pageContext = (PageContext) ActionContext.getContext().get(ServletActionContext.PAGE_CONTEXT); + + if (pageContext == null && servlet != null) { + pageContext = jspFactory.getPageContext(servlet, request, response, null, true, 8192, true); + ActionContext.getContext().put(ServletActionContext.PAGE_CONTEXT, pageContext); + usedJspFactory = true; + } + + try { + String encoding = getEncoding(finalLocation); + String contentType = getContentType(finalLocation); + + if (encoding != null) { + contentType = contentType + ";charset=" + encoding; + } + + Template t = getTemplate(stack, velocityManager.getVelocityEngine(), invocation, finalLocation, encoding); + + Context context = createContext(velocityManager, stack, request, response, finalLocation); + Writer writer = new OutputStreamWriter(response.getOutputStream(), encoding); + + + response.setContentType(contentType); + + t.merge(context, writer); + + // always flush the writer (we used to only flush it if this was a jspWriter, but someone asked + // to do it all the time (WW-829). Since Velocity support is being deprecated, we'll oblige :) + writer.flush(); + } catch (Exception e) { + LOG.error("Unable to render velocity template: '{}'", finalLocation, e); + throw e; + } finally { + if (usedJspFactory) { + jspFactory.releasePageContext(pageContext); + } + } + } + + /** + * Retrieve the content type for this template. + * <p/> + * People can override this method if they want to provide specific content types for specific templates (eg text/xml). + * + * @return The content type associated with this template (default "text/html") + */ + protected String getContentType(String templateLocation) { + return "text/html"; + } + + /** + * Retrieve the encoding for this template. + * <p/> + * People can override this method if they want to provide specific encodings for specific templates. + * + * @return The encoding associated with this template (defaults to the value of 'struts.i18n.encoding' property) + */ + protected String getEncoding(String templateLocation) { + String encoding = defaultEncoding; + if (encoding == null) { + encoding = System.getProperty("file.encoding"); + } + if (encoding == null) { + encoding = "UTF-8"; + } + return encoding; + } + + /** + * Given a value stack, a Velocity engine, and an action invocation, this method returns the appropriate + * Velocity template to render. + * + * @param stack the value stack to resolve the location again (when parse equals true) + * @param velocity the velocity engine to process the request against + * @param invocation an encapsulation of the action execution state. + * @param location the location of the template + * @param encoding the charset encoding of the template + * @return the template to render + * @throws Exception when the requested template could not be found + */ + protected Template getTemplate(ValueStack stack, VelocityEngine velocity, ActionInvocation invocation, String location, String encoding) throws Exception { + if (!location.startsWith("/")) { + location = invocation.getProxy().getNamespace() + "/" + location; + } + + Template template = velocity.getTemplate(location, encoding); + + return template; + } + + /** + * Creates the VelocityContext that we'll use to render this page. + * + * @param velocityManager a reference to the velocityManager to use + * @param stack the value stack to resolve the location against (when parse equals true) + * @param location the name of the template that is being used + * @return the a minted Velocity context. + */ + protected Context createContext(VelocityManager velocityManager, ValueStack stack, HttpServletRequest request, HttpServletResponse response, String location) { + return velocityManager.createContext(stack, request, response); + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerResult.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerResult.java b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerResult.java index e5258e6..0775f55 100644 --- a/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerResult.java +++ b/core/src/main/java/org/apache/struts2/views/freemarker/FreemarkerResult.java @@ -38,7 +38,7 @@ import freemarker.template.TemplateModelException; import org.apache.commons.lang3.ObjectUtils; import org.apache.struts2.ServletActionContext; import org.apache.struts2.StrutsStatics; -import org.apache.struts2.dispatcher.StrutsResultSupport; +import org.apache.struts2.result.StrutsResultSupport; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/main/resources/struts-default.xml ---------------------------------------------------------------------- diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml index 5b82fe0..33057cb 100644 --- a/core/src/main/resources/struts-default.xml +++ b/core/src/main/resources/struts-default.xml @@ -168,16 +168,16 @@ <package name="struts-default" abstract="true"> <result-types> <result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/> - <result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/> + <result-type name="dispatcher" class="org.apache.struts2.result.ServletDispatcherResult" default="true"/> <result-type name="freemarker" class="org.apache.struts2.views.freemarker.FreemarkerResult"/> - <result-type name="httpheader" class="org.apache.struts2.dispatcher.HttpHeaderResult"/> - <result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/> - <result-type name="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult"/> - <result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/> - <result-type name="velocity" class="org.apache.struts2.dispatcher.VelocityResult"/> + <result-type name="httpheader" class="org.apache.struts2.result.HttpHeaderResult"/> + <result-type name="redirect" class="org.apache.struts2.result.ServletRedirectResult"/> + <result-type name="redirectAction" class="org.apache.struts2.result.ServletActionRedirectResult"/> + <result-type name="stream" class="org.apache.struts2.result.StreamResult"/> + <result-type name="velocity" class="org.apache.struts2.result.VelocityResult"/> <result-type name="xslt" class="org.apache.struts2.views.xslt.XSLTResult"/> - <result-type name="plainText" class="org.apache.struts2.dispatcher.PlainTextResult" /> - <result-type name="postback" class="org.apache.struts2.dispatcher.PostbackResult" /> + <result-type name="plainText" class="org.apache.struts2.result.PlainTextResult" /> + <result-type name="postback" class="org.apache.struts2.result.PostbackResult" /> </result-types> <interceptors> http://git-wip-us.apache.org/repos/asf/struts/blob/6bc99ab9/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java b/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java index 1806573..aee8473 100644 --- a/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java +++ b/core/src/test/java/org/apache/struts2/TestConfigurationProvider.java @@ -39,7 +39,7 @@ import com.opensymphony.xwork2.security.DefaultExcludedPatternsChecker; import com.opensymphony.xwork2.security.ExcludedPatternsChecker; import com.opensymphony.xwork2.util.location.LocatableProperties; import com.opensymphony.xwork2.validator.ValidationInterceptor; -import org.apache.struts2.dispatcher.ServletDispatcherResult; +import org.apache.struts2.result.ServletDispatcherResult; import org.apache.struts2.interceptor.TokenInterceptor; import org.apache.struts2.interceptor.TokenSessionStoreInterceptor; import org.apache.struts2.views.jsp.ui.DoubleValidationAction;
