This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch kusal-depr-apis-7 in repository https://gitbox.apache.org/repos/asf/struts.git
commit e4d2877b609d11329f076c1bb582fd7d02a65040 Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Thu Oct 17 17:53:30 2024 +1100 WW-3714 Deprecate and migrate assorted Interceptors --- .../xwork2/conversion/impl/XWorkConverter.java | 19 +- .../interceptor/PrefixMethodInvocationUtil.java | 51 ++-- .../opensymphony/xwork2/util/TextParseUtil.java | 3 + .../xwork2/util/ValueStackFactory.java | 7 +- .../struts2/interceptor/AliasInterceptor.java | 294 +++++++++++++++++++ .../struts2/interceptor/ChainingInterceptor.java | 275 +++++++++++++++++ .../interceptor/ConversionErrorInterceptor.java | 150 ++++++++++ .../interceptor/DefaultWorkflowInterceptor.java | 248 ++++++++++++++++ .../interceptor/ExceptionMappingInterceptor.java | 324 +++++++++++++++++++++ .../struts2/interceptor/LoggingInterceptor.java | 90 ++++++ .../interceptor/ModelDrivenInterceptor.java | 148 ++++++++++ .../interceptor/ParameterRemoverInterceptor.java | 124 ++++++++ .../struts2/interceptor/PrepareInterceptor.java | 177 +++++++++++ .../interceptor/ScopedModelDrivenInterceptor.java | 166 +++++++++++ .../interceptor/StaticParametersInterceptor.java | 243 ++++++++++++++++ 15 files changed, 2290 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java index 3cda3e34d..5d16d0e4d 100644 --- a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkConverter.java @@ -18,12 +18,21 @@ */ package com.opensymphony.xwork2.conversion.impl; -import com.opensymphony.xwork2.*; -import com.opensymphony.xwork2.conversion.*; +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.FileManager; +import com.opensymphony.xwork2.FileManagerFactory; +import com.opensymphony.xwork2.LocalizedTextProvider; +import com.opensymphony.xwork2.conversion.ConversionAnnotationProcessor; +import com.opensymphony.xwork2.conversion.ConversionFileProcessor; +import com.opensymphony.xwork2.conversion.TypeConverter; +import com.opensymphony.xwork2.conversion.TypeConverterHolder; import com.opensymphony.xwork2.conversion.annotations.Conversion; import com.opensymphony.xwork2.conversion.annotations.TypeConversion; import com.opensymphony.xwork2.inject.Inject; -import com.opensymphony.xwork2.util.*; +import com.opensymphony.xwork2.util.AnnotationUtils; +import com.opensymphony.xwork2.util.ClassLoaderUtil; +import com.opensymphony.xwork2.util.CompoundRoot; +import com.opensymphony.xwork2.util.ValueStack; import com.opensymphony.xwork2.util.reflection.ReflectionContextState; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -220,6 +229,10 @@ public class XWorkConverter extends DefaultTypeConverter { return message; } + public static String getConversionErrorMessage(String propertyName, Class toClass, org.apache.struts2.util.ValueStack stack) { + return getConversionErrorMessage(propertyName, toClass, ValueStack.adapt(stack)); + } + private static String removeAllIndexesInPropertyName(String propertyName) { return propertyName.replaceAll(MESSAGE_INDEX_PATTERN, PERIOD); } diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/PrefixMethodInvocationUtil.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/PrefixMethodInvocationUtil.java index 040080824..0ac840c7a 100644 --- a/core/src/main/java/com/opensymphony/xwork2/interceptor/PrefixMethodInvocationUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/PrefixMethodInvocationUtil.java @@ -19,8 +19,8 @@ package com.opensymphony.xwork2.interceptor; import com.opensymphony.xwork2.ActionInvocation; -import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -28,7 +28,7 @@ import java.lang.reflect.Method; /** * <p> * A utility class for invoking prefixed methods in action class. - * + * * Interceptors that made use of this class are: * </p> * <ul> @@ -37,7 +37,7 @@ import java.lang.reflect.Method; * </ul> * * * <!-- START SNIPPET: javadocDefaultWorkflowInterceptor --> - * + * * <b>In DefaultWorkflowInterceptor</b> * <p>applies only when action implements {@link com.opensymphony.xwork2.Validateable}</p> * <ol> @@ -45,12 +45,12 @@ import java.lang.reflect.Method; * <li>else if the action class have validateDo{MethodName}(), it will be invoked</li> * <li>no matter if 1] or 2] is performed, if alwaysInvokeValidate property of the interceptor is "true" (which is by default "true"), validate() will be invoked.</li> * </ol> - * + * * <!-- END SNIPPET: javadocDefaultWorkflowInterceptor --> - * - * + * + * * <!-- START SNIPPET: javadocPrepareInterceptor --> - * + * * <b>In PrepareInterceptor</b> * <p>Applies only when action implements Preparable</p> * <ol> @@ -58,14 +58,14 @@ import java.lang.reflect.Method; * <li>else if the action class have prepareDo(MethodName()}(), it will be invoked</li> * <li>no matter if 1] or 2] is performed, if alwaysinvokePrepare property of the interceptor is "true" (which is by default "true"), prepare() will be invoked.</li> * </ol> - * + * * <!-- END SNIPPET: javadocPrepareInterceptor --> - * + * * @author Philip Luppens * @author tm_jee */ public class PrefixMethodInvocationUtil { - + private static final Logger LOG = LogManager.getLogger(PrefixMethodInvocationUtil.class); private static final String DEFAULT_INVOCATION_METHODNAME = "execute"; @@ -76,7 +76,7 @@ public class PrefixMethodInvocationUtil { * <p> * This method will prefix <code>actionInvocation</code>'s <code>ActionProxy</code>'s * <code>method</code> with <code>prefixes</code> before invoking the prefixed method. - * Order of the <code>prefixes</code> is important, as this method will return once + * Order of the <code>prefixes</code> is important, as this method will return once * a prefixed method is found in the action class. * </p> * @@ -89,7 +89,7 @@ public class PrefixMethodInvocationUtil { * </pre> * * <p> - * Assuming <code>actionInvocation.getProxy(),getMethod()</code> returns "submit", + * Assuming <code>actionInvocation.getProxy(),getMethod()</code> returns "submit", * the order of invocation would be as follows:- * </p> * @@ -99,12 +99,12 @@ public class PrefixMethodInvocationUtil { * </ol> * * <p> - * If <code>prepareSubmit()</code> exists, it will be invoked and this method - * will return, <code>prepareDoSubmit()</code> will NOT be invoked. + * If <code>prepareSubmit()</code> exists, it will be invoked and this method + * will return, <code>prepareDoSubmit()</code> will NOT be invoked. * </p> * * <p> - * On the other hand, if <code>prepareDoSubmit()</code> does not exists, and + * On the other hand, if <code>prepareDoSubmit()</code> does not exists, and * <code>prepareDoSubmit()</code> exists, it will be invoked. * </p> * @@ -119,29 +119,32 @@ public class PrefixMethodInvocationUtil { */ public static void invokePrefixMethod(ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException { Object action = actionInvocation.getAction(); - + String methodName = actionInvocation.getProxy().getMethod(); - + if (methodName == null) { - // if null returns (possible according to the docs), use the default execute + // if null returns (possible according to the docs), use the default execute methodName = DEFAULT_INVOCATION_METHODNAME; } - + Method method = getPrefixedMethod(prefixes, methodName, action); if (method != null) { method.invoke(action, new Object[0]); } } - - + + public static void invokePrefixMethod(org.apache.struts2.ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException { + invokePrefixMethod(ActionInvocation.adapt(actionInvocation), prefixes); + } + /** - * This method returns a {@link Method} in <code>action</code>. The method + * This method returns a {@link Method} in <code>action</code>. The method * returned is found by searching for method in <code>action</code> whose method name * is equals to the result of appending each <code>prefixes</code> * to <code>methodName</code>. Only the first method found will be returned, hence * the order of <code>prefixes</code> is important. If none is found this method * will return null. - * + * * @param prefixes the prefixes to prefix the <code>methodName</code> * @param methodName the method name to be prefixed with <code>prefixes</code> * @param action the action class of which the prefixed method is to be search for. @@ -162,7 +165,7 @@ public class PrefixMethodInvocationUtil { } return null; } - + /** * <p> * This method capitalized the first character of <code>methodName</code>. diff --git a/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java b/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java index 2a2cad1bf..9220159bf 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java @@ -51,6 +51,9 @@ public class TextParseUtil { return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, null).toString(); } + public static String translateVariables(String expression, org.apache.struts2.util.ValueStack stack) { + return translateVariables(expression, ValueStack.adapt(stack)); + } /** * Function similarly as {@link #translateVariables(char, String, ValueStack)} diff --git a/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java b/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java index 788c90453..70d072851 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/ValueStackFactory.java @@ -29,7 +29,7 @@ public interface ValueStackFactory { * @return a new {@link com.opensymphony.xwork2.util.ValueStack}. */ ValueStack createValueStack(); - + /** * Get a new instance of {@link com.opensymphony.xwork2.util.ValueStack} * @@ -37,5 +37,8 @@ public interface ValueStackFactory { * @return a new {@link com.opensymphony.xwork2.util.ValueStack}. */ ValueStack createValueStack(ValueStack stack); - + + default ValueStack createValueStack(org.apache.struts2.util.ValueStack stack) { + return createValueStack(ValueStack.adapt(stack)); + } } diff --git a/core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java new file mode 100644 index 000000000..7fa112a83 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/AliasInterceptor.java @@ -0,0 +1,294 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.LocalizedTextProvider; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.ParametersInterceptor; +import com.opensymphony.xwork2.interceptor.ValidationAware; +import com.opensymphony.xwork2.security.AcceptedPatternsChecker; +import com.opensymphony.xwork2.security.ExcludedPatternsChecker; +import com.opensymphony.xwork2.util.ClearableValueStack; +import com.opensymphony.xwork2.util.Evaluated; +import com.opensymphony.xwork2.util.ValueStackFactory; +import com.opensymphony.xwork2.util.reflection.ReflectionContextState; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.dispatcher.HttpParameters; +import org.apache.struts2.dispatcher.Parameter; +import org.apache.struts2.util.ValueStack; + +import java.util.Map; + + +/** + * <!-- START SNIPPET: description --> + * + * The aim of this Interceptor is to alias a named parameter to a different named parameter. By acting as the glue + * between actions sharing similar parameters (but with different names), it can help greatly with action chaining. + * + * <p>Action's alias expressions should be in the form of <code>#{ "name1" : "alias1", "name2" : "alias2" }</code>. + * This means that assuming an action (or something else in the stack) has a value for the expression named <i>name1</i> and the + * action this interceptor is applied to has a setter named <i>alias1</i>, <i>alias1</i> will be set with the value from + * <i>name1</i>. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>aliasesKey (optional) - the name of the action parameter to look for the alias map (by default this is + * <i>aliases</i>).</li> + * + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * This interceptor does not have any known extension points. + * + * <!-- END SNIPPET: extending --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <action name="someAction" class="com.examples.SomeAction"> + * <!-- The value for the foo parameter will be applied as if it were named bar --> + * <param name="aliases">#{ 'foo' : 'bar' }</param> + * + * <interceptor-ref name="alias"/> + * <interceptor-ref name="basicStack"/> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Matthew Payne + */ +public class AliasInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(AliasInterceptor.class); + + private static final String DEFAULT_ALIAS_KEY = "aliases"; + protected String aliasesKey = DEFAULT_ALIAS_KEY; + + protected ValueStackFactory valueStackFactory; + protected LocalizedTextProvider localizedTextProvider; + protected boolean devMode = false; + + private ExcludedPatternsChecker excludedPatterns; + private AcceptedPatternsChecker acceptedPatterns; + + @Inject(StrutsConstants.STRUTS_DEVMODE) + public void setDevMode(String mode) { + this.devMode = Boolean.parseBoolean(mode); + } + + @Inject + public void setValueStackFactory(ValueStackFactory valueStackFactory) { + this.valueStackFactory = valueStackFactory; + } + + @Inject + public void setLocalizedTextProvider(LocalizedTextProvider localizedTextProvider) { + this.localizedTextProvider = localizedTextProvider; + } + + @Inject + public void setExcludedPatterns(ExcludedPatternsChecker excludedPatterns) { + this.excludedPatterns = excludedPatterns; + } + + @Inject + public void setAcceptedPatterns(AcceptedPatternsChecker acceptedPatterns) { + this.acceptedPatterns = acceptedPatterns; + } + + /** + * <p> + * Sets the name of the action parameter to look for the alias map. + * </p> + * + * <p> + * Default is <code>aliases</code>. + * </p> + * + * @param aliasesKey the name of the action parameter + */ + public void setAliasesKey(String aliasesKey) { + this.aliasesKey = aliasesKey; + } + + @Override public String intercept(ActionInvocation invocation) throws Exception { + + ActionConfig config = invocation.getProxy().getConfig(); + ActionContext ac = invocation.getInvocationContext(); + Object action = invocation.getAction(); + + // get the action's parameters + final Map<String, String> parameters = config.getParams(); + + if (parameters.containsKey(aliasesKey)) { + + String aliasExpression = parameters.get(aliasesKey); + ValueStack stack = ac.getValueStack(); + Object obj = stack.findValue(aliasExpression); + + if (obj instanceof Map) { + //get secure stack + ValueStack newStack = valueStackFactory.createValueStack(stack); + boolean clearableStack = newStack instanceof ClearableValueStack; + if (clearableStack) { + //if the stack's context can be cleared, do that to prevent OGNL + //from having access to objects in the stack, see XW-641 + ((ClearableValueStack)newStack).clearContextValues(); + Map<String, Object> context = newStack.getContext(); + ReflectionContextState.setCreatingNullObjects(context, true); + ReflectionContextState.setDenyMethodExecution(context, true); + ReflectionContextState.setReportingConversionErrors(context, true); + + //keep locale from original context + newStack.getActionContext().withLocale(stack.getActionContext().getLocale()); + } + + // override + Map aliases = (Map) obj; + for (Object o : aliases.entrySet()) { + Map.Entry entry = (Map.Entry) o; + String name = entry.getKey().toString(); + if (isNotAcceptableExpression(name)) { + continue; + } + String alias = (String) entry.getValue(); + if (isNotAcceptableExpression(alias)) { + continue; + } + Evaluated value = new Evaluated(stack.findValue(name)); + if (!value.isDefined()) { + // workaround + HttpParameters contextParameters = ActionContext.getContext().getParameters(); + + if (null != contextParameters) { + Parameter param = contextParameters.get(name); + if (param.isDefined()) { + value = new Evaluated(param.getValue()); + } + } + } + if (value.isDefined()) { + try { + newStack.setValue(alias, value.get()); + } catch (RuntimeException e) { + if (devMode) { + String developerNotification = localizedTextProvider.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{ + "Unexpected Exception caught setting '" + entry.getKey() + "' on '" + action.getClass() + ": " + e.getMessage() + }); + LOG.error(developerNotification); + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationAware) { + ((ValidationAware) action).addActionMessage(developerNotification); + } + } + } + } + } + + if (clearableStack) { + stack.getActionContext().withConversionErrors(newStack.getActionContext().getConversionErrors()); + } + } else { + LOG.debug("invalid alias expression: {}", aliasesKey); + } + } + + return invocation.invoke(); + } + + protected boolean isAccepted(String paramName) { + AcceptedPatternsChecker.IsAccepted result = acceptedPatterns.isAccepted(paramName); + if (result.isAccepted()) { + return true; + } + + LOG.warn("Parameter [{}] didn't match accepted pattern [{}]! See Accepted / Excluded patterns at\n" + + "https://struts.apache.org/security/#accepted--excluded-patterns", + paramName, result.getAcceptedPattern()); + + return false; + } + + protected boolean isExcluded(String paramName) { + ExcludedPatternsChecker.IsExcluded result = excludedPatterns.isExcluded(paramName); + if (!result.isExcluded()) { + return false; + } + + LOG.warn("Parameter [{}] matches excluded pattern [{}]! See Accepted / Excluded patterns at\n" + + "https://struts.apache.org/security/#accepted--excluded-patterns", + paramName, result.getExcludedPattern()); + + return true; + } + + /** + * Checks if expression contains vulnerable code + * + * @param expression of interceptor + * @return true|false + */ + protected boolean isNotAcceptableExpression(String expression) { + return isExcluded(expression) || !isAccepted(expression); + } + + /** + * Sets a comma-delimited list of regular expressions to match + * parameters that are allowed in the parameter map (aka whitelist). + * <p> + * Don't change the default unless you know what you are doing in terms + * of security implications. + * </p> + * + * @param commaDelim A comma-delimited list of regular expressions + */ + public void setAcceptParamNames(String commaDelim) { + acceptedPatterns.setAcceptedPatterns(commaDelim); + } + + /** + * Sets a comma-delimited list of regular expressions to match + * parameters that should be removed from the parameter map. + * + * @param commaDelim A comma-delimited list of regular expressions + */ + public void setExcludeParams(String commaDelim) { + excludedPatterns.setExcludedPatterns(commaDelim); + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java new file mode 100644 index 000000000..adbb3a67a --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java @@ -0,0 +1,275 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.ActionChainResult; +import com.opensymphony.xwork2.Unchainable; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.CompoundRoot; +import com.opensymphony.xwork2.util.ProxyUtil; +import com.opensymphony.xwork2.util.TextParseUtil; +import com.opensymphony.xwork2.util.reflection.ReflectionProvider; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.Result; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.util.ValueStack; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + + +/** + * <!-- START SNIPPET: description --> + * <p> + * An interceptor that copies all the properties of every object in the value stack to the currently executing object, + * except for any object that implements {@link Unchainable}. A collection of optional <i>includes</i> and + * <i>excludes</i> may be provided to control how and which parameters are copied. Only includes or excludes may be + * specified. Specifying both results in undefined behavior. See the javadocs for {@link ReflectionProvider#copy(Object, Object, + * Map, Collection, Collection)} for more information. + * </p> + * + * <p> + * <b>Note:</b> It is important to remember that this interceptor does nothing if there are no objects already on the stack. + * <br>This means two things: + * <br><b>One</b>, you can safely apply it to all your actions without any worry of adverse affects. + * <br><b>Two</b>, it is up to you to ensure an object exists in the stack prior to invoking this action. The most typical way this is done + * is through the use of the <b>chain</b> result type, which combines with this interceptor to make up the action + * chaining feature. + * </p> + * + * <p> + * <b>Note:</b> By default Errors, Field errors and Message aren't copied during chaining, to change the behaviour you can specify + * the below three constants in struts.properties or struts.xml: + * </p> + * + * <ul> + * <li>struts.chaining.copyErrors - set to true to copy Action Errors</li> + * <li>struts.chaining.copyFieldErrors - set to true to copy Field Errors</li> + * <li>struts.chaining.copyMessages - set to true to copy Action Messages</li> + * </ul> + * + * <p> + * <u>Example:</u> + * </p> + * + * <pre> + * <constant name="struts.xwork.chaining.copyErrors" value="true"/> + * </pre> + * + * <p> + * <b>Note:</b> By default actionErrors and actionMessages are excluded when copping object's properties. + * </p> + * <!-- END SNIPPET: description --> + * <u>Interceptor parameters:</u> + * <!-- START SNIPPET: parameters --> + * <ul> + * <li>excludes (optional) - the list of parameter names to exclude from copying (all others will be included).</li> + * <li>includes (optional) - the list of parameter names to include when copying (all others will be excluded).</li> + * </ul> + * <!-- END SNIPPET: parameters --> + * <u>Extending the interceptor:</u> + * <!-- START SNIPPET: extending --> + * <p> + * There are no known extension points to this interceptor. + * </p> + * <!-- END SNIPPET: extending --> + * <u>Example code:</u> + * + * <!-- START SNIPPET: example --> + * <pre> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="basicStack"/> + * <result name="success" type="chain">otherAction</result> + * </action> + * </pre> + * + * <pre> + * <action name="otherAction" class="com.examples.OtherAction"> + * <interceptor-ref name="chain"/> + * <interceptor-ref name="basicStack"/> + * <result name="success">good_result.ftl</result> + * </action> + * </pre> + * <!-- END SNIPPET: example --> + * + * + * @author mrdon + * @author tm_jee ( tm_jee(at)yahoo.co.uk ) + * @see ActionChainResult + */ +public class ChainingInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(ChainingInterceptor.class); + + private static final String ACTION_ERRORS = "actionErrors"; + private static final String FIELD_ERRORS = "fieldErrors"; + private static final String ACTION_MESSAGES = "actionMessages"; + + private boolean copyMessages = false; + private boolean copyErrors = false; + private boolean copyFieldErrors = false; + + protected Collection<String> excludes; + + protected Collection<String> includes; + protected ReflectionProvider reflectionProvider; + + @Inject + public void setReflectionProvider(ReflectionProvider prov) { + this.reflectionProvider = prov; + } + + @Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false) + public void setCopyErrors(String copyErrors) { + this.copyErrors = "true".equalsIgnoreCase(copyErrors); + } + + @Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_FIELD_ERRORS, required = false) + public void setCopyFieldErrors(String copyFieldErrors) { + this.copyFieldErrors = "true".equalsIgnoreCase(copyFieldErrors); + } + + @Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_MESSAGES, required = false) + public void setCopyMessages(String copyMessages) { + this.copyMessages = "true".equalsIgnoreCase(copyMessages); + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + ValueStack stack = invocation.getStack(); + CompoundRoot root = stack.getRoot(); + if (shouldCopyStack(invocation, root)) { + copyStack(invocation, root); + } + return invocation.invoke(); + } + + private void copyStack(ActionInvocation invocation, CompoundRoot root) { + List list = prepareList(root); + Map<String, Object> ctxMap = invocation.getInvocationContext().getContextMap(); + for (Object object : list) { + if (shouldCopy(object)) { + Object action = invocation.getAction(); + Class<?> editable = null; + if(ProxyUtil.isProxy(action)) { + editable = ProxyUtil.ultimateTargetClass(action); + } + reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable); + } + } + } + + private Collection<String> prepareExcludes() { + Collection<String> localExcludes = excludes; + if (!copyErrors || !copyMessages ||!copyFieldErrors) { + if (localExcludes == null) { + localExcludes = new HashSet<String>(); + if (!copyErrors) { + localExcludes.add(ACTION_ERRORS); + } + if (!copyMessages) { + localExcludes.add(ACTION_MESSAGES); + } + if (!copyFieldErrors) { + localExcludes.add(FIELD_ERRORS); + } + } + } + return localExcludes; + } + + private boolean shouldCopy(Object o) { + return o != null && !(o instanceof Unchainable); + } + + @SuppressWarnings("unchecked") + private List prepareList(CompoundRoot root) { + List list = new ArrayList(root); + list.remove(0); + Collections.reverse(list); + return list; + } + + private boolean shouldCopyStack(ActionInvocation invocation, CompoundRoot root) throws Exception { + Result result = invocation.getResult(); + return root.size() > 1 && (result == null || ActionChainResult.class.isAssignableFrom(result.getClass())); + } + + /** + * Gets list of parameter names to exclude + * + * @return the exclude list + */ + public Collection<String> getExcludes() { + return excludes; + } + + /** + * Sets the list of parameter names to exclude from copying (all others will be included). + * + * @param excludes the excludes list as comma separated String + */ + public void setExcludes(String excludes) { + this.excludes = TextParseUtil.commaDelimitedStringToSet(excludes); + } + + /** + * Sets the list of parameter names to exclude from copying (all others will be included). + * + * @param excludes the excludes list + */ + public void setExcludesCollection(Collection<String> excludes) { + this.excludes = excludes; + } + + /** + * Gets list of parameter names to include + * + * @return the include list + */ + public Collection<String> getIncludes() { + return includes; + } + + /** + * Sets the list of parameter names to include when copying (all others will be excluded). + * + * @param includes the includes list as comma separated String + */ + public void setIncludes(String includes) { + this.includes = TextParseUtil.commaDelimitedStringToSet(includes); + } + + + /** + * Sets the list of parameter names to include when copying (all others will be excluded). + * + * @param includes the includes list + */ + public void setIncludesCollection(Collection<String> includes) { + this.includes = includes; + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ConversionErrorInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ConversionErrorInterceptor.java new file mode 100644 index 000000000..0301441e5 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ConversionErrorInterceptor.java @@ -0,0 +1,150 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.conversion.impl.ConversionData; +import com.opensymphony.xwork2.conversion.impl.XWorkConverter; +import com.opensymphony.xwork2.interceptor.ValidationAware; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.util.ValueStack; + +import java.util.HashMap; +import java.util.Map; + + +/** + * <!-- START SNIPPET: description --> + * ConversionErrorInterceptor adds conversion errors from the ActionContext to the Action's field errors. + * + * <p> + * This interceptor adds any error found in the {@link ActionContext}'s conversionErrors map as a field error (provided + * that the action implements {@link com.opensymphony.xwork2.interceptor.ValidationAware}). In addition, any field that contains a validation error has its + * original value saved such that any subsequent requests for that value return the original value rather than the value + * in the action. This is important because if the value "abc" is submitted and can't be converted to an int, we want to + * display the original string ("abc") again rather than the int value (likely 0, which would make very little sense to + * the user). + * </p> + * + * <p> + * <b>Note:</b> Since 2.5.2, this interceptor extends {@link com.opensymphony.xwork2.interceptor.MethodFilterInterceptor}, therefore being + * able to deal with excludeMethods / includeMethods parameters. See [Workflow Interceptor] + * (class {@link DefaultWorkflowInterceptor}) for documentation and examples on how to use this feature. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * <li>None</li> + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p> <u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * Because this interceptor is not web-specific, it abstracts the logic for whether an error should be added. This + * allows for web-specific interceptors to use more complex logic in the {@link #shouldAddError} method for when a value + * has a conversion error but is null or empty or otherwise indicates that the value was never actually entered by the + * user. + * + * <!-- END SNIPPET: extending --> + * + * <p> <u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="params"/> + * <interceptor-ref name="conversionError"/> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Jason Carreira + */ +public class ConversionErrorInterceptor extends MethodFilterInterceptor { + + public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override"; + + protected Object getOverrideExpr(ActionInvocation invocation, Object value) { + return escape(value); + } + + protected String escape(Object value) { + return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\""; + } + + @Override + public String doIntercept(ActionInvocation invocation) throws Exception { + + ActionContext invocationContext = invocation.getInvocationContext(); + Map<String, ConversionData> conversionErrors = invocationContext.getConversionErrors(); + ValueStack stack = invocationContext.getValueStack(); + + HashMap<Object, Object> fakie = null; + + for (Map.Entry<String, ConversionData> entry : conversionErrors.entrySet()) { + String propertyName = entry.getKey(); + ConversionData conversionData = entry.getValue(); + + if (shouldAddError(propertyName, conversionData.getValue())) { + String message = XWorkConverter.getConversionErrorMessage(propertyName, conversionData.getToClass(), stack); + + Object action = invocation.getAction(); + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationAware) { + com.opensymphony.xwork2.interceptor.ValidationAware va = (ValidationAware) action; + va.addFieldError(propertyName, message); + } + + if (fakie == null) { + fakie = new HashMap<>(); + } + + fakie.put(propertyName, getOverrideExpr(invocation, conversionData.getValue())); + } + } + + if (fakie != null) { + // if there were some errors, put the original (fake) values in place right before the result + stack.getContext().put(ORIGINAL_PROPERTY_OVERRIDE, fakie); + invocation.addPreResultListener(new PreResultListener() { + public void beforeResult(ActionInvocation invocation, String resultCode) { + Map<Object, Object> fakie = (Map<Object, Object>) invocation.getInvocationContext().get(ORIGINAL_PROPERTY_OVERRIDE); + + if (fakie != null) { + invocation.getStack().setExprOverrides(fakie); + } + } + }); + } + return invocation.invoke(); + } + + protected boolean shouldAddError(String propertyName, Object value) { + return true; + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/DefaultWorkflowInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/DefaultWorkflowInterceptor.java new file mode 100644 index 000000000..c51554d01 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/DefaultWorkflowInterceptor.java @@ -0,0 +1,248 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.Action; +import com.opensymphony.xwork2.interceptor.ValidationAware; +import com.opensymphony.xwork2.interceptor.ValidationErrorAware; +import com.opensymphony.xwork2.interceptor.ValidationWorkflowAware; +import com.opensymphony.xwork2.interceptor.annotations.InputConfig; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; + +/** + * <!-- START SNIPPET: description --> + * <p> + * An interceptor that makes sure there are not validation, conversion or action errors before allowing the interceptor chain to continue. + * If a single FieldError or ActionError (including the ones replicated by the Message Store Interceptor in a redirection) is found, the INPUT result will be triggered. + * <b>This interceptor does not perform any validation</b>. + * </p> + * + * <p> + * This interceptor does nothing if the name of the method being invoked is specified in the <b>excludeMethods</b> + * parameter. <b>excludeMethods</b> accepts a comma-delimited list of method names. For example, requests to + * <b>foo!input.action</b> and <b>foo!back.action</b> will be skipped by this interceptor if you set the + * <b>excludeMethods</b> parameter to "input, back". + * </p> + * + * <p> + * <b>Note:</b> As this method extends off MethodFilterInterceptor, it is capable of + * deciding if it is applicable only to selective methods in the action class. This is done by adding param tags + * for the interceptor element, naming either a list of excluded method names and/or a list of included method + * names, whereby includeMethods overrides excludedMethods. A single * sign is interpreted as wildcard matching + * all methods for both parameters. + * See {@link com.opensymphony.xwork2.interceptor.MethodFilterInterceptor} for more info. + * </p> + * + * <p> + * This interceptor also supports the following interfaces which can implemented by actions: + * </p> + * + * <ul> + * <li>ValidationAware - implemented by ActionSupport class</li> + * <li>ValidationWorkflowAware - allows changing result name programmatically</li> + * <li>ValidationErrorAware - notifies action about errors and also allow change result name</li> + * </ul> + * + * <p> + * You can also use InputConfig annotation to change result name returned when validation errors occurred. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * <ul> + * <li>inputResultName - Default to "input". Determine the result name to be returned when + * an action / field error is found.</li> + * </ul> + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * <p>There are no known extension points for this interceptor.</p> + * + * <!-- END SNIPPET: extending --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="params"/> + * <interceptor-ref name="validation"/> + * <interceptor-ref name="workflow"/> + * <result name="success">good_result.ftl</result> + * </action> + * + * <-- In this case myMethod as well as mySecondMethod of the action class + * will not pass through the workflow process --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="params"/> + * <interceptor-ref name="validation"/> + * <interceptor-ref name="workflow"> + * <param name="excludeMethods">myMethod,mySecondMethod</param> + * </interceptor-ref name="workflow"> + * <result name="success">good_result.ftl</result> + * </action> + * + * <-- In this case, the result named "error" will be used when + * an action / field error is found --> + * <-- The Interceptor will only be applied for myWorkflowMethod method of action + * classes, since this is the only included method while any others are excluded --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="params"/> + * <interceptor-ref name="validation"/> + * <interceptor-ref name="workflow"> + * <param name="inputResultName">error</param> + * <param name="excludeMethods">*</param> + * <param name="includeMethods">myWorkflowMethod</param> + * </interceptor-ref> + * <result name="success">good_result.ftl</result> + * </action> + * + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Jason Carreira + * @author Rainer Hermanns + * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a> + * @author Philip Luppens + * @author tm_jee + */ +public class DefaultWorkflowInterceptor extends MethodFilterInterceptor { + + private static final long serialVersionUID = 7563014655616490865L; + + private static final Logger LOG = LogManager.getLogger(DefaultWorkflowInterceptor.class); + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + private String inputResultName = Action.INPUT; + + /** + * Set the <code>inputResultName</code> (result name to be returned when + * a action / field error is found registered). Default to {@link Action#INPUT} + * + * @param inputResultName what result name to use when there was validation error(s). + */ + public void setInputResultName(String inputResultName) { + this.inputResultName = inputResultName; + } + + /** + * Intercept {@link ActionInvocation} and returns a <code>inputResultName</code> + * when action / field errors is found registered. + * + * @param invocation the action invocation + * @return String result name + */ + @Override + protected String doIntercept(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationAware) { + com.opensymphony.xwork2.interceptor.ValidationAware validationAwareAction = (ValidationAware) action; + + if (validationAwareAction.hasErrors()) { + LOG.debug("Errors on action [{}], returning result name [{}]", validationAwareAction, inputResultName); + + String resultName = inputResultName; + resultName = processValidationWorkflowAware(action, resultName); + resultName = processInputConfig(action, invocation.getProxy().getMethod(), resultName); + resultName = processValidationErrorAware(action, resultName); + + return resultName; + } + } + + return invocation.invoke(); + } + + /** + * Process {@link com.opensymphony.xwork2.interceptor.ValidationWorkflowAware} interface + * + * @param action action object + * @param currentResultName current result name + * + * @return result name + */ + private String processValidationWorkflowAware(final Object action, final String currentResultName) { + String resultName = currentResultName; + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationWorkflowAware) { + resultName = ((com.opensymphony.xwork2.interceptor.ValidationWorkflowAware) action).getInputResultName(); + LOG.debug("Changing result name from [{}] to [{}] because of processing [{}] interface applied to [{}]", + currentResultName, resultName, ValidationWorkflowAware.class.getSimpleName(), action); + } + return resultName; + } + + /** + * Process {@link InputConfig} annotation applied to method + * @param action action object + * @param method method + * @param currentResultName current result name + * + * @return result name + * + * @throws Exception in case of any errors + */ + protected String processInputConfig(final Object action, final String method, final String currentResultName) throws Exception { + String resultName = currentResultName; + InputConfig annotation = MethodUtils.getAnnotation(action.getClass().getMethod(method, EMPTY_CLASS_ARRAY), + InputConfig.class ,true,true); + if (annotation != null) { + if (StringUtils.isNotEmpty(annotation.methodName())) { + resultName = (String) MethodUtils.invokeMethod(action, true, annotation.methodName()); + } else { + resultName = annotation.resultName(); + } + LOG.debug("Changing result name from [{}] to [{}] because of processing annotation [{}] on action [{}]", + currentResultName, resultName, InputConfig.class.getSimpleName(), action); + } + return resultName; + } + + /** + * Notify action if it implements {@link com.opensymphony.xwork2.interceptor.ValidationErrorAware} interface + * + * @param action action object + * @param currentResultName current result name + * + * @return result name + * @see com.opensymphony.xwork2.interceptor.ValidationErrorAware + */ + protected String processValidationErrorAware(final Object action, final String currentResultName) { + String resultName = currentResultName; + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationErrorAware) { + resultName = ((com.opensymphony.xwork2.interceptor.ValidationErrorAware) action).actionErrorOccurred(currentResultName); + LOG.debug("Changing result name from [{}] to [{}] because of processing interface [{}] on action [{}]", + currentResultName, resultName, ValidationErrorAware.class.getSimpleName(), action); + } + return resultName; + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java new file mode 100644 index 000000000..277fc33df --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java @@ -0,0 +1,324 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig; +import com.opensymphony.xwork2.interceptor.ExceptionHolder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.dispatcher.HttpParameters; + +import java.util.List; +import java.util.Map; + +/** + * <!-- START SNIPPET: description --> + * <p> + * This interceptor forms the core functionality of the exception handling feature. Exception handling allows you to map + * an exception to a result code, just as if the action returned a result code instead of throwing an unexpected + * exception. When an exception is encountered, it is wrapped with an {@link ExceptionHolder} and pushed on the stack, + * providing easy access to the exception from within your result. + * </p> + * + * <p> + * <b>Note:</b> While you can configure exception mapping in your configuration file at any point, the configuration + * will not have any effect if this interceptor is not in the interceptor stack for your actions. It is recommended that + * you make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any + * exception, even those caused by other interceptors. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>logEnabled (optional) - Should exceptions also be logged? (boolean true|false)</li> + * + * <li>logLevel (optional) - what log level should we use (<code>trace, debug, info, warn, error, fatal</code>)? - defaut is <code>debug</code></li> + * + * <li>logCategory (optional) - If provided we would use this category (eg. <code>com.mycompany.app</code>). + * Default is to use <code>com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor</code>.</li> + * + * </ul> + * + * <p> + * The parameters above enables us to log all thrown exceptions with stacktace in our own logfile, + * and present a friendly webpage (with no stacktrace) to the end user. + * </p> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * <p> + * If you want to add custom handling for publishing the Exception, you may override + * {@link #publishException(ActionInvocation, ExceptionHolder)}. The default implementation + * pushes the given ExceptionHolder on value stack. A custom implementation could add additional logging etc. + * </p> + * <!-- END SNIPPET: extending --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <xwork> + * <package name="default" extends="xwork-default"> + * <global-results> + * <result name="error" type="freemarker">error.ftl</result> + * </global-results> + * + * <global-exception-mappings> + * <exception-mapping exception="java.lang.Exception" result="error"/> + * </global-exception-mappings> + * + * <action name="test"> + * <interceptor-ref name="exception"/> + * <interceptor-ref name="basicStack"/> + * <exception-mapping exception="com.acme.CustomException" result="custom_error"/> + * <result name="custom_error">custom_error.ftl</result> + * <result name="success" type="freemarker">test.ftl</result> + * </action> + * </package> + * </xwork> + * <!-- END SNIPPET: example --> + * </pre> + * + * <p> + * This second example will also log the exceptions using our own category + * <code>com.mycompany.app.unhandled</code> at WARN level. + * </p> + * + * <pre> + * <!-- START SNIPPET: example2 --> + * <xwork> + * <package name="something" extends="xwork-default"> + * <interceptors> + * <interceptor-stack name="exceptionmappingStack"> + * <interceptor-ref name="exception"> + * <param name="logEnabled">true</param> + * <param name="logCategory">com.mycompany.app.unhandled</param> + * <param name="logLevel">WARN</param> + * </interceptor-ref> + * <interceptor-ref name="i18n"/> + * <interceptor-ref name="staticParams"/> + * <interceptor-ref name="params"/> + * <interceptor-ref name="validation"> + * <param name="excludeMethods">input,back,cancel,browse</param> + * </interceptor-ref> + * </interceptor-stack> + * </interceptors> + * + * <default-interceptor-ref name="exceptionmappingStack"/> + * + * <global-results> + * <result name="unhandledException">/unhandled-exception.jsp</result> + * </global-results> + * + * <global-exception-mappings> + * <exception-mapping exception="java.lang.Exception" result="unhandledException"/> + * </global-exception-mappings> + * + * <action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction"> + * <exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException" + * result="damm"/> + * <result name="input">index.jsp</result> + * <result name="success">success.jsp</result> + * <result name="damm">damm.jsp</result> + * </action> + * + * </package> + * </xwork> + * <!-- END SNIPPET: example2 --> + * </pre> + * + * @author Matthew E. Porter (matthew dot porter at metissian dot com) + * @author Claus Ibsen + */ +public class ExceptionMappingInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(ExceptionMappingInterceptor.class); + + protected Logger categoryLogger; + protected boolean logEnabled = false; + protected String logCategory; + protected String logLevel; + + + public boolean isLogEnabled() { + return logEnabled; + } + + public void setLogEnabled(boolean logEnabled) { + this.logEnabled = logEnabled; + } + + public String getLogCategory() { + return logCategory; + } + + public void setLogCategory(String logCatgory) { + this.logCategory = logCatgory; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + String result; + + try { + result = invocation.invoke(); + } catch (Exception e) { + if (isLogEnabled()) { + handleLogging(e); + } + List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings(); + ExceptionMappingConfig mappingConfig = this.findMappingFromExceptions(exceptionMappings, e); + if (mappingConfig != null && mappingConfig.getResult()!=null) { + Map<String, String> mappingParams = mappingConfig.getParams(); + // create a mutable HashMap since some interceptors will remove parameters, and parameterMap is immutable + HttpParameters parameters = HttpParameters.create(mappingParams).build(); + invocation.getInvocationContext().withParameters(parameters); + result = mappingConfig.getResult(); + publishException(invocation, new ExceptionHolder(e)); + } else { + throw e; + } + } + + return result; + } + + /** + * Handles the logging of the exception. + * + * @param e the exception to log. + */ + protected void handleLogging(Exception e) { + if (logCategory != null) { + if (categoryLogger == null) { + // init category logger + categoryLogger = LogManager.getLogger(logCategory); + } + doLog(categoryLogger, e); + } else { + doLog(LOG, e); + } + } + + /** + * Performs the actual logging. + * + * @param logger the provided logger to use. + * @param e the exception to log. + */ + protected void doLog(Logger logger, Exception e) { + if (logLevel == null) { + logger.debug(e.getMessage(), e); + return; + } + + if ("trace".equalsIgnoreCase(logLevel)) { + logger.trace(e.getMessage(), e); + } else if ("debug".equalsIgnoreCase(logLevel)) { + logger.debug(e.getMessage(), e); + } else if ("info".equalsIgnoreCase(logLevel)) { + logger.info(e.getMessage(), e); + } else if ("warn".equalsIgnoreCase(logLevel)) { + logger.warn(e.getMessage(), e); + } else if ("error".equalsIgnoreCase(logLevel)) { + logger.error(e.getMessage(), e); + } else if ("fatal".equalsIgnoreCase(logLevel)) { + logger.fatal(e.getMessage(), e); + } else { + throw new IllegalArgumentException("LogLevel [" + logLevel + "] is not supported"); + } + } + + /** + * Try to find appropriate {@link ExceptionMappingConfig} based on provided Throwable + * + * @param exceptionMappings list of defined exception mappings + * @param t caught exception + * @return appropriate mapping or null + */ + protected ExceptionMappingConfig findMappingFromExceptions(List<ExceptionMappingConfig> exceptionMappings, Throwable t) { + ExceptionMappingConfig config = null; + // Check for specific exception mappings. + if (exceptionMappings != null) { + int deepest = Integer.MAX_VALUE; + for (Object exceptionMapping : exceptionMappings) { + ExceptionMappingConfig exceptionMappingConfig = (ExceptionMappingConfig) exceptionMapping; + int depth = getDepth(exceptionMappingConfig.getExceptionClassName(), t); + if (depth >= 0 && depth < deepest) { + deepest = depth; + config = exceptionMappingConfig; + } + } + } + return config; + } + + /** + * Return the depth to the superclass matching. 0 means ex matches exactly. Returns -1 if there's no match. + * Otherwise, returns depth. Lowest depth wins. + * + * @param exceptionMapping the mapping classname + * @param t the cause + * @return the depth, if not found -1 is returned. + */ + public int getDepth(String exceptionMapping, Throwable t) { + return getDepth(exceptionMapping, t.getClass(), 0); + } + + private int getDepth(String exceptionMapping, Class exceptionClass, int depth) { + if (exceptionClass.getName().contains(exceptionMapping)) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (exceptionClass.equals(Throwable.class)) { + return -1; + } + return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); + } + + /** + * Default implementation to handle ExceptionHolder publishing. Pushes given ExceptionHolder on the stack. + * Subclasses may override this to customize publishing. + * + * @param invocation The invocation to publish Exception for. + * @param exceptionHolder The exceptionHolder wrapping the Exception to publish. + */ + protected void publishException(ActionInvocation invocation, ExceptionHolder exceptionHolder) { + invocation.getStack().push(exceptionHolder); + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/LoggingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/LoggingInterceptor.java new file mode 100644 index 000000000..4536d462b --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/LoggingInterceptor.java @@ -0,0 +1,90 @@ +/* + * 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.interceptor; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; + + +/** + * <!-- START SNIPPET: description --> + * <p> + * This interceptor logs the start and end of the execution an action (in English-only, not internationalized). + * <br> + * <b>Note:</b>: This interceptor will log at <tt>INFO</tt> level. + * </p> + * <!-- END SNIPPET: description --> + * + * <!-- START SNIPPET: parameters --> + * There are no parameters for this interceptor. + * <!-- END SNIPPET: parameters --> + * + * <!-- START SNIPPET: extending --> + * There are no obvious extensions to the existing interceptor. + * <!-- END SNIPPET: extending --> + * + * <pre> + * <!-- START SNIPPET: example --> + * <!-- prints out a message before and after the immediate action execution --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="completeStack"/> + * <interceptor-ref name="logger"/> + * <result name="success">good_result.ftl</result> + * </action> + * + * <!-- prints out a message before any more interceptors continue and after they have finished --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="logger"/> + * <interceptor-ref name="completeStack"/> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Jason Carreira + */ +public class LoggingInterceptor extends AbstractInterceptor { + private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class); + private static final String FINISH_MESSAGE = "Finishing execution stack for action "; + private static final String START_MESSAGE = "Starting execution stack for action "; + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + logMessage(invocation, START_MESSAGE); + String result = invocation.invoke(); + logMessage(invocation, FINISH_MESSAGE); + return result; + } + + private void logMessage(ActionInvocation invocation, String baseMessage) { + if (LOG.isInfoEnabled()) { + StringBuilder message = new StringBuilder(baseMessage); + String namespace = invocation.getProxy().getNamespace(); + + if ((namespace != null) && (namespace.trim().length() > 0)) { + message.append(namespace).append("/"); + } + + message.append(invocation.getProxy().getActionName()); + LOG.info(message.toString()); + } + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java new file mode 100644 index 000000000..aeaf0a040 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java @@ -0,0 +1,148 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.ModelDriven; +import com.opensymphony.xwork2.util.CompoundRoot; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.interceptor.parameter.ParametersInterceptor; +import org.apache.struts2.util.ValueStack; + +/** + * <!-- START SNIPPET: description --> + * + * Watches for {@link ModelDriven} actions and adds the action's model on to the value stack. + * + * <p> <b>Note:</b> The ModelDrivenInterceptor must come before the both {@link StaticParametersInterceptor} and + * {@link ParametersInterceptor} if you want the parameters to be applied to the model. + * </p> + * <p> <b>Note:</b> The ModelDrivenInterceptor will only push the model into the stack when the + * model is not null, else it will be ignored. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>refreshModelBeforeResult - set to true if you want the model to be refreshed on the value stack after action + * execution and before result execution. The setting is useful if you want to change the model instance during the + * action execution phase, like when loading it from the data layer. This will result in getModel() being called at + * least twice.</li> + * + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * There are no known extension points to this interceptor. + * + * <!-- END SNIPPET: extending --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="modelDriven"/> + * <interceptor-ref name="basicStack"/> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author tm_jee + * @version $Date$ $Id$ + */ +public class ModelDrivenInterceptor extends AbstractInterceptor { + + protected boolean refreshModelBeforeResult = false; + + public void setRefreshModelBeforeResult(boolean val) { + this.refreshModelBeforeResult = val; + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + + if (action instanceof ModelDriven) { + ModelDriven modelDriven = (ModelDriven) action; + ValueStack stack = invocation.getStack(); + Object model = modelDriven.getModel(); + if (model != null) { + stack.push(model); + } + if (refreshModelBeforeResult) { + invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model)); + } + } + return invocation.invoke(); + } + + /** + * Refreshes the model instance on the value stack, if it has changed + */ + protected static class RefreshModelBeforeResult implements PreResultListener { + private Object originalModel; + protected ModelDriven action; + + + public RefreshModelBeforeResult(ModelDriven action, Object model) { + this.originalModel = model; + this.action = action; + } + + public void beforeResult(ActionInvocation invocation, String resultCode) { + ValueStack stack = invocation.getStack(); + CompoundRoot root = stack.getRoot(); + + boolean needsRefresh = true; + Object newModel = action.getModel(); + + // Check to see if the new model instance is already on the stack + if (newModel != null) { + for (Object item : root) { + if (item == newModel) { + needsRefresh = false; + break; + } + } + } + + // Add the new model on the stack + if (needsRefresh) { + + // Clear off the old model instance + if (originalModel != null) { + root.remove(originalModel); + } + if (newModel != null) { + stack.push(newModel); + } + } + } + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ParameterRemoverInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ParameterRemoverInterceptor.java new file mode 100644 index 000000000..ad72d5853 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ParameterRemoverInterceptor.java @@ -0,0 +1,124 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.util.TextParseUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.action.NoParameters; +import org.apache.struts2.dispatcher.HttpParameters; +import org.apache.struts2.dispatcher.Parameter; + +import java.util.Collections; +import java.util.Set; + +/** + * This is a simple XWork interceptor that allows parameters (matching + * one of the paramNames attribute csv value) to be + * removed from the parameter map if they match a certain value + * (matching one of the paramValues attribute csv value), before they + * are set on the action. A typical usage would be to want a dropdown/select + * to map onto a boolean value on an action. The select had the options + * none, yes and no with values -1, true and false. The true and false would + * map across correctly. However the -1 would be set to false. + * This was not desired as one might needed the value on the action to stay null. + * This interceptor fixes this by preventing the parameter from ever reaching + * the action. + * + * <ul> + * <li>paramNames - A comma separated value (csv) indicating the parameter name + * whose param value should be considered that if they match any of the + * comma separated value (csv) from paramValues attribute, shall be + * removed from the parameter map such that they will not be applied + * to the action</li> + * <li>paramValues - A comma separated value (csv) indicating the parameter value that if + * matched shall have its parameter be removed from the parameter map + * such that they will not be applied to the action</li> + * </ul> + * <p> + * No intended extension point + * + * <pre> + * <action name="sample" class="org.martingilday.Sample"> + * <interceptor-ref name="paramRemover"> + * <param name="paramNames">aParam,anotherParam</param> + * <param name="paramValues">--,-1</param> + * </interceptor-ref> + * <interceptor-ref name="defaultStack" /> + * ... + * </action> + * </pre> + */ +public class ParameterRemoverInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(ParameterRemoverInterceptor.class); + + private Set<String> paramNames = Collections.emptySet(); + private Set<String> paramValues = Collections.emptySet(); + + /** + * Decide if the parameter should be removed from the parameter map based on + * <code>paramNames</code> and <code>paramValues</code>. + * + * @see AbstractInterceptor + */ + @Override + public String intercept(ActionInvocation invocation) throws Exception { + if (!(invocation.getAction() instanceof NoParameters) + && (null != this.paramNames)) { + ActionContext ac = invocation.getInvocationContext(); + HttpParameters parameters = ac.getParameters(); + + if (parameters != null) { + for (String removeName : paramNames) { + try { + Parameter parameter = parameters.get(removeName); + if (parameter.isDefined() && this.paramValues.contains(parameter.getValue())) { + parameters.remove(removeName); + } + } catch (Exception e) { + LOG.error("Failed to convert parameter to string", e); + } + } + } + } + return invocation.invoke(); + } + + /** + * Allows <code>paramNames</code> attribute to be set as comma-separated-values (csv). + * + * @param paramNames the paramNames to set + */ + public void setParamNames(String paramNames) { + this.paramNames = TextParseUtil.commaDelimitedStringToSet(paramNames); + } + + /** + * Allows <code>paramValues</code> attribute to be set as a comma-separated-values (csv). + * + * @param paramValues the paramValues to set + */ + public void setParamValues(String paramValues) { + this.paramValues = TextParseUtil.commaDelimitedStringToSet(paramValues); + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/PrepareInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/PrepareInterceptor.java new file mode 100644 index 000000000..f45a501d4 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/PrepareInterceptor.java @@ -0,0 +1,177 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.Preparable; +import com.opensymphony.xwork2.interceptor.PrefixMethodInvocationUtil; +import org.apache.struts2.ActionInvocation; + +import java.lang.reflect.InvocationTargetException; + +/** + * <!-- START SNIPPET: description --> + * + * This interceptor calls <code>prepare()</code> on actions which implement + * {@link Preparable}. This interceptor is very useful for any situation where + * you need to ensure some logic runs before the actual execute method runs. + * + * <p> + * A typical use of this is to run some logic to load an object from the + * database so that when parameters are set they can be set on this object. For + * example, suppose you have a User object with two properties: <i>id</i> and + * <i>name</i>. Provided that the params interceptor is called twice (once + * before and once after this interceptor), you can load the User object using + * the id property, and then when the second params interceptor is called the + * parameter <i>user.name</i> will be set, as desired, on the actual object + * loaded from the database. See the example for more info. + * </p> + * <p> + * <b>Note:</b> Since XWork 2.0.2, this interceptor extends {@link com.opensymphony.xwork2.interceptor.MethodFilterInterceptor}, therefore being + * able to deal with excludeMethods / includeMethods parameters. See [Workflow Interceptor] + * (class {@link DefaultWorkflowInterceptor}) for documentation and examples on how to use this feature. + * </p> + * + * <p> + * <b>Update</b>: Added logic to execute a prepare{MethodName} and conditionally + * the a general prepare() Method, depending on the 'alwaysInvokePrepare' parameter/property + * which is by default true. This allows us to run some logic based on the method + * name we specify in the {@link com.opensymphony.xwork2.ActionProxy}. For example, you can specify a + * prepareInput() method that will be run before the invocation of the input method. + * </p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>alwaysInvokePrepare - Default to true. If true, prepare will always be invoked, + * otherwise it will not.</li> + * + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * There are no known extension points to this interceptor. + * + * <!-- END SNIPPET: extending --> + * + * <p> <u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <!-- Calls the params interceptor twice, allowing you to + * pre-load data for the second time parameters are set --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="params"/> + * <interceptor-ref name="prepare"/> + * <interceptor-ref name="basicStack"/> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Jason Carreira + * @author Philip Luppens + * @author tm_jee + * @see Preparable + */ +public class PrepareInterceptor extends MethodFilterInterceptor { + + private static final long serialVersionUID = -5216969014510719786L; + + private final static String PREPARE_PREFIX = "prepare"; + private final static String ALT_PREPARE_PREFIX = "prepareDo"; + + private boolean alwaysInvokePrepare = true; + private boolean firstCallPrepareDo = false; + + /** + * Sets if the <code>prepare</code> method should always be executed. + * <p> + * Default is <tt>true</tt>. + * </p> + * + * @param alwaysInvokePrepare if <code>prepare</code> should always be executed or not. + */ + public void setAlwaysInvokePrepare(String alwaysInvokePrepare) { + this.alwaysInvokePrepare = Boolean.parseBoolean(alwaysInvokePrepare); + } + + /** + * Sets if the <code>prepareDoXXX</code> method should be called first + * <p> + * Default is <tt>false</tt> for backward compatibility + * </p> + * @param firstCallPrepareDo if <code>prepareDoXXX</code> should be called first + */ + public void setFirstCallPrepareDo(String firstCallPrepareDo) { + this.firstCallPrepareDo = Boolean.parseBoolean(firstCallPrepareDo); + } + + @Override + public String doIntercept(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + + if (action instanceof Preparable) { + try { + String[] prefixes; + if (firstCallPrepareDo) { + prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX}; + } else { + prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX}; + } + PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes); + } + catch (InvocationTargetException e) { + /* + * The invoked method threw an exception and reflection wrapped it + * in an InvocationTargetException. + * If possible re-throw the original exception so that normal + * exception handling will take place. + */ + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } else if(cause instanceof Error) { + throw (Error) cause; + } else { + /* + * The cause is not an Exception or Error (must be Throwable) so + * just re-throw the wrapped exception. + */ + throw e; + } + } + + if (alwaysInvokePrepare) { + ((Preparable) action).prepare(); + } + } + + return invocation.invoke(); + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ScopedModelDrivenInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ScopedModelDrivenInterceptor.java new file mode 100644 index 000000000..f474770f0 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ScopedModelDrivenInterceptor.java @@ -0,0 +1,166 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.ScopedModelDriven; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.StrutsException; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * <!-- START SNIPPET: description --> + * + * An interceptor that enables scoped model-driven actions. + * + * <p>This interceptor only activates on actions that implement the {@link com.opensymphony.xwork2.interceptor.ScopedModelDriven} interface. If + * detected, it will retrieve the model class from the configured scope, then provide it to the Action.</p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>className - The model class name. Defaults to the class name of the object returned by the getModel() method.</li> + * + * <li>name - The key to use when storing or retrieving the instance in a scope. Defaults to the model + * class name.</li> + * + * <li>scope - The scope to store and retrieve the model. Defaults to 'request' but can also be 'session'.</li> + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * There are no known extension points for this interceptor. + * + * <!-- END SNIPPET: extending --> + * + * <p><u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * + * <-- Basic usage --> + * <interceptor name="scopedModelDriven" class="com.opensymphony.interceptor.ScopedModelDrivenInterceptor" /> + * + * <-- Using all available parameters --> + * <interceptor name="gangsterForm" class="com.opensymphony.interceptor.ScopedModelDrivenInterceptor"> + * <param name="scope">session</param> + * <param name="name">gangsterForm</param> + * <param name="className">com.opensymphony.example.GangsterForm</param> + * </interceptor> + * + * <!-- END SNIPPET: example --> + * </pre> + */ +public class ScopedModelDrivenInterceptor extends AbstractInterceptor { + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + private static final String GET_MODEL = "getModel"; + private String scope; + private String name; + private String className; + private ObjectFactory objectFactory; + + @Inject + public void setObjectFactory(ObjectFactory factory) { + this.objectFactory = factory; + } + + protected Object resolveModel(ObjectFactory factory, ActionContext actionContext, String modelClassName, String modelScope, String modelName) throws Exception { + Object model; + Map<String, Object> scopeMap = actionContext.getContextMap(); + if ("session".equals(modelScope)) { + scopeMap = actionContext.getSession(); + } + + model = scopeMap.get(modelName); + if (model == null) { + model = factory.buildBean(modelClassName, null); + scopeMap.put(modelName, model); + } + return model; + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + + if (action instanceof com.opensymphony.xwork2.interceptor.ScopedModelDriven) { + com.opensymphony.xwork2.interceptor.ScopedModelDriven modelDriven = (ScopedModelDriven) action; + if (modelDriven.getModel() == null) { + ActionContext ctx = ActionContext.getContext(); + ActionConfig config = invocation.getProxy().getConfig(); + + String cName = className; + if (cName == null) { + try { + Method method = action.getClass().getMethod(GET_MODEL, EMPTY_CLASS_ARRAY); + Class cls = method.getReturnType(); + cName = cls.getName(); + } catch (NoSuchMethodException e) { + throw new StrutsException("The " + GET_MODEL + "() is not defined in action " + action.getClass() + "", config); + } + } + String modelName = name; + if (modelName == null) { + modelName = cName; + } + Object model = resolveModel(objectFactory, ctx, cName, scope, modelName); + modelDriven.setModel(model); + modelDriven.setScopeKey(modelName); + } + } + return invocation.invoke(); + } + + /** + * @param className the className to set + */ + public void setClassName(String className) { + this.className = className; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @param scope the scope to set + */ + public void setScope(String scope) { + this.scope = scope; + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/StaticParametersInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/StaticParametersInterceptor.java new file mode 100644 index 000000000..ec0581a73 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/StaticParametersInterceptor.java @@ -0,0 +1,243 @@ +/* + * 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.interceptor; + +import com.opensymphony.xwork2.LocalizedTextProvider; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.config.entities.Parameterizable; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.ParametersInterceptor; +import com.opensymphony.xwork2.interceptor.ValidationAware; +import com.opensymphony.xwork2.util.ClearableValueStack; +import com.opensymphony.xwork2.util.TextParseUtil; +import com.opensymphony.xwork2.util.ValueStackFactory; +import com.opensymphony.xwork2.util.reflection.ReflectionContextState; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionContext; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.dispatcher.HttpParameters; +import org.apache.struts2.util.ValueStack; + +import java.util.Collections; +import java.util.Map; + +/** + * <!-- START SNIPPET: description --> + * + * This interceptor populates the action with the static parameters defined in the action configuration. If the action + * implements {@link Parameterizable}, a map of the static parameters will be also be passed directly to the action. + * The static params will be added to the request params map, unless "merge" is set to false. + * + * <p> Parameters are typically defined with <param> elements within xwork.xml.</p> + * + * <!-- END SNIPPET: description --> + * + * <p><u>Interceptor parameters:</u></p> + * + * <!-- START SNIPPET: parameters --> + * + * <ul> + * + * <li>None</li> + * + * </ul> + * + * <!-- END SNIPPET: parameters --> + * + * <p><u>Extending the interceptor:</u></p> + * + * <!-- START SNIPPET: extending --> + * + * <p>There are no extension points to this interceptor.</p> + * + * <!-- END SNIPPET: extending --> + * + * <p> <u>Example code:</u></p> + * + * <pre> + * <!-- START SNIPPET: example --> + * <action name="someAction" class="com.examples.SomeAction"> + * <interceptor-ref name="staticParams"> + * <param name="parse">true</param> + * <param name="overwrite">false</param> + * </interceptor-ref> + * <result name="success">good_result.ftl</result> + * </action> + * <!-- END SNIPPET: example --> + * </pre> + * + * @author Patrick Lightbody + */ +public class StaticParametersInterceptor extends AbstractInterceptor { + + private boolean parse; + private boolean overwrite; + private boolean merge = true; + private boolean devMode = false; + + private static final Logger LOG = LogManager.getLogger(StaticParametersInterceptor.class); + + private ValueStackFactory valueStackFactory; + private LocalizedTextProvider localizedTextProvider; + + @Inject + public void setValueStackFactory(ValueStackFactory valueStackFactory) { + this.valueStackFactory = valueStackFactory; + } + + @Inject(StrutsConstants.STRUTS_DEVMODE) + public void setDevMode(String mode) { + devMode = BooleanUtils.toBoolean(mode); + } + + @Inject + public void setLocalizedTextProvider(LocalizedTextProvider localizedTextProvider) { + this.localizedTextProvider = localizedTextProvider; + } + + public void setParse(String value) { + this.parse = BooleanUtils.toBoolean(value); + } + + public void setMerge(String value) { + this.merge = BooleanUtils.toBoolean(value); + } + + /** + * Overwrites already existing parameters from other sources. + * Static parameters are the successor over previously set parameters, if true. + * + * @param value enable overwrites of already existing parameters from other sources + */ + public void setOverwrite(String value) { + this.overwrite = BooleanUtils.toBoolean(value); + } + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + ActionConfig config = invocation.getProxy().getConfig(); + Object action = invocation.getAction(); + + final Map<String, String> parameters = config.getParams(); + + LOG.debug("Setting static parameters: {}", parameters); + + // for actions marked as Parameterizable, pass the static parameters directly + if (action instanceof Parameterizable) { + ((Parameterizable) action).setParams(parameters); + } + + if (parameters != null) { + ActionContext ac = ActionContext.getContext(); + Map<String, Object> contextMap = ac.getContextMap(); + try { + ReflectionContextState.setCreatingNullObjects(contextMap, true); + ReflectionContextState.setReportingConversionErrors(contextMap, true); + final ValueStack stack = ac.getValueStack(); + + ValueStack newStack = valueStackFactory.createValueStack(stack); + boolean clearableStack = newStack instanceof ClearableValueStack; + if (clearableStack) { + //if the stack's context can be cleared, do that to prevent OGNL + //from having access to objects in the stack, see XW-641 + ((ClearableValueStack)newStack).clearContextValues(); + Map<String, Object> context = newStack.getContext(); + ReflectionContextState.setCreatingNullObjects(context, true); + ReflectionContextState.setDenyMethodExecution(context, true); + ReflectionContextState.setReportingConversionErrors(context, true); + + //keep locale from original context + newStack.getActionContext().withLocale(stack.getActionContext().getLocale()); + } + + for (Map.Entry<String, String> entry : parameters.entrySet()) { + Object val = entry.getValue(); + if (parse && val instanceof String) { + val = TextParseUtil.translateVariables(val.toString(), stack); + } + try { + newStack.setValue(entry.getKey(), val); + } catch (RuntimeException e) { + if (devMode) { + + String developerNotification = localizedTextProvider.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{ + "Unexpected Exception caught setting '" + entry.getKey() + "' on '" + action.getClass() + ": " + e.getMessage() + }); + LOG.error(developerNotification); + if (action instanceof com.opensymphony.xwork2.interceptor.ValidationAware) { + ((ValidationAware) action).addActionMessage(developerNotification); + } + } + } + } + + if (clearableStack) { + stack.getActionContext().withConversionErrors(newStack.getActionContext().getConversionErrors()); + } + + if (merge) + addParametersToContext(ac, parameters); + } finally { + ReflectionContextState.setCreatingNullObjects(contextMap, false); + ReflectionContextState.setReportingConversionErrors(contextMap, false); + } + } + return invocation.invoke(); + } + + + /** + * @param ac The action context + * @return the parameters from the action mapping in the context. If none found, returns + * an empty map. + */ + protected Map<String, String> retrieveParameters(ActionContext ac) { + ActionConfig config = ac.getActionInvocation().getProxy().getConfig(); + if (config != null) { + return config.getParams(); + } else { + return Collections.emptyMap(); + } + } + + /** + * Adds the parameters into context's ParameterMap. + * As default, static parameters will not overwrite existing parameters from other sources. + * If you want the static parameters as successor over already existing parameters, set overwrite to <tt>true</tt>. + * + * @param ac The action context + * @param newParams The parameter map to apply + */ + protected void addParametersToContext(ActionContext ac, Map<String, ?> newParams) { + HttpParameters previousParams = ac.getParameters(); + + HttpParameters.Builder combinedParams; + if (overwrite) { + combinedParams = HttpParameters.create().withParent( previousParams); + combinedParams = combinedParams.withExtraParams(newParams); + } else { + combinedParams = HttpParameters.create(newParams); + combinedParams = combinedParams.withExtraParams(previousParams); + } + ac.withParameters(combinedParams.build()); + } +}