1 /*
2 * Copyright (c) 2002-2007 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.interceptor;
6
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.Comparator;
10 import java.util.HashSet;
11 import java.util.Iterator;
12 import java.util.Map;
13 import java.util.Set;
14 import java.util.TreeMap;
15 import java.util.regex.Matcher;
16 import java.util.regex.Pattern;
17
18 import com.opensymphony.xwork2.ActionContext;
19 import com.opensymphony.xwork2.ActionInvocation;
20 import com.opensymphony.xwork2.ValidationAware;
21 import com.opensymphony.xwork2.conversion.impl.InstantiatingNullHandler;
22 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
23 import com.opensymphony.xwork2.inject.Inject;
24 import com.opensymphony.xwork2.util.LocalizedTextUtil;
25 import com.opensymphony.xwork2.util.TextParseUtil;
26 import com.opensymphony.xwork2.util.ValueStack;
27 import com.opensymphony.xwork2.util.logging.Logger;
28 import com.opensymphony.xwork2.util.logging.LoggerFactory;
29 import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
30
31
32 /**
33 * <!-- START SNIPPET: description -->
34 * This interceptor sets all parameters on the value stack.
35 * <p/>
36 * This interceptor gets all parameters from {@link ActionContext#getParameters()} and sets them on the value stack by
37 * calling {@link ValueStack#setValue(String, Object)}, typically resulting in the values submitted in a form
38 * request being applied to an action in the value stack. Note that the parameter map must contain a String key and
39 * often containers a String[] for the value.
40 *
41 * <p/> The interceptor takes one parameter named 'ordered'. When set to true action properties are guaranteed to be
42 * set top-down which means that top action's properties are set first. Then it's subcomponents properties are set.
43 * The reason for this order is to enable a 'factory' pattern. For example, let's assume that one has an action
44 * that contains a property named 'modelClass' that allows to choose what is the underlying implementation of model.
45 * By assuring that modelClass property is set before any model properties are set, it's possible to choose model
46 * implementation during action.setModelClass() call. Similiarily it's possible to use action.setPrimaryKey()
47 * property set call to actually load the model class from persistent storage. Without any assumption on parameter
48 * order you have to use patterns like 'Preparable'.
49 *
50 * <p/> Because parameter names are effectively OGNL statements, it is important that security be taken in to account.
51 * This interceptor will not apply any values in the parameters map if the expression contains an assignment (=),
52 * multiple expressions (,), or references any objects in the context (#). This is all done in the {@link
53 * #acceptableName(String)} method. In addition to this method, if the action being invoked implements the {@link
54 * ParameterNameAware} interface, the action will be consulted to determine if the parameter should be set.
55 *
56 * <p/> In addition to these restrictions, a flag ({@link ReflectionContextState#DENY_METHOD_EXECUTION}) is set such that
57 * no methods are allowed to be invoked. That means that any expression such as <i>person.doSomething()</i> or
58 * <i>person.getName()</i> will be explicitely forbidden. This is needed to make sure that your application is not
59 * exposed to attacks by malicious users.
60 *
61 * <p/> While this interceptor is being invoked, a flag ({@link ReflectionContextState#CREATE_NULL_OBJECTS}) is turned
62 * on to ensure that any null reference is automatically created - if possible. See the type conversion documentation
63 * and the {@link InstantiatingNullHandler} javadocs for more information.
64 *
65 * <p/> Finally, a third flag ({@link XWorkConverter#REPORT_CONVERSION_ERRORS}) is set that indicates any errors when
66 * converting the the values to their final data type (String[] -> int) an unrecoverable error occured. With this
67 * flag set, the type conversion errors will be reported in the action context. See the type conversion documentation
68 * and the {@link XWorkConverter} javadocs for more information.
69 *
70 * <p/> If you are looking for detailed logging information about your parameters, turn on DEBUG level logging for this
71 * interceptor. A detailed log of all the parameter keys and values will be reported.
72 *
73 * <p/>
74 * <b>Note:</b> Since XWork 2.0.2, this interceptor extends {@link MethodFilterInterceptor}, therefore being
75 * able to deal with excludeMethods / includeMethods parameters. See [Workflow Interceptor]
76 * (class {@link DefaultWorkflowInterceptor}) for documentation and examples on how to use this feature.
77 *
78 * <!-- END SNIPPET: description -->
79 *
80 * <p/> <u>Interceptor parameters:</u>
81 *
82 * <!-- START SNIPPET: parameters -->
83 *
84 * <ul>
85 *
86 * <li>ordered - set to true if you want the top-down property setter behaviour</li>
87 *
88 * </ul>
89 *
90 * <!-- END SNIPPET: parameters -->
91 *
92 * <p/> <u>Extending the interceptor:</u>
93 *
94 * <!-- START SNIPPET: extending -->
95 *
96 * <p/> The best way to add behavior to this interceptor is to utilize the {@link ParameterNameAware} interface in your
97 * actions. However, if you wish to apply a global rule that isn't implemented in your action, then you could extend
98 * this interceptor and override the {@link #acceptableName(String)} method.
99 *
100 * <!-- END SNIPPET: extending -->
101 *
102 * <p/> <u>Example code:</u>
103 *
104 * <pre>
105 * <!-- START SNIPPET: example -->
106 * <action name="someAction" class="com.examples.SomeAction">
107 * <interceptor-ref name="params"/>
108 * <result name="success">good_result.ftl</result>
109 * </action>
110 * <!-- END SNIPPET: example -->
111 * </pre>
112 *
113 * @author Patrick Lightbody
114 */
115 public class ParametersInterceptor extends MethodFilterInterceptor {
116
117 private static final Logger LOG = LoggerFactory.getLogger(ParametersInterceptor.class);
118
119 boolean ordered = false;
120 Set<Pattern> excludeParams = Collections.EMPTY_SET;
121 static boolean devMode = false;
122
123 @Inject("devMode")
124 public static void setDevMode(String mode) {
125 devMode = "true".equals(mode);
126 }
127
128 /** Compares based on number of '.' characters (fewer is higher) */
129 static final Comparator rbCollator = new Comparator() {
130 public int compare(Object arg0, Object arg1) {
131 String s1 = (String) arg0;
132 String s2 = (String) arg1;
133 int l1=0, l2=0;
134 for( int i=s1.length()-1; i>=0; i--) {
135 if( s1.charAt(i) == '.') l1++;
136 }
137 for( int i=s2.length()-1; i>=0; i--) {
138 if( s2.charAt(i) == '.') l2++;
139 }
140 return l1 < l2 ? -1 : ( l2 < l1 ? 1 : s1.compareTo(s2));
141 }
142 };
143
144 public String doIntercept(ActionInvocation invocation) throws Exception {
145 Object action = invocation.getAction();
146 if (!(action instanceof NoParameters)) {
147 ActionContext ac = invocation.getInvocationContext();
148 final Map parameters = retrieveParametersFromContext(ac);
149
150 if (LOG.isDebugEnabled()) {
151 LOG.debug("Setting params " + getParameterLogMap(parameters));
152 }
153
154 if (parameters != null) {
155 Map contextMap = ac.getContextMap();
156 try {
157 ReflectionContextState.setCreatingNullObjects(contextMap, true);
158 ReflectionContextState.setDenyMethodExecution(contextMap, true);
159 ReflectionContextState.setReportingConversionErrors(contextMap, true);
160
161 ValueStack stack = ac.getValueStack();
162 setParameters(action, stack, parameters);
163 } finally {
164 ReflectionContextState.setCreatingNullObjects(contextMap, false);
165 ReflectionContextState.setDenyMethodExecution(contextMap, false);
166 ReflectionContextState.setReportingConversionErrors(contextMap, false);
167 }
168 }
169 }
170 return invocation.invoke();
171 }
172
173 /**
174 * Gets the parameter map to apply from the context
175 * @param ac The action context
176 * @return The parameter map to apply
177 */
178 protected Map retrieveParametersFromContext(ActionContext ac) {
179 return ac.getParameters();
180 }
181
182 protected void setParameters(Object action, ValueStack stack, final Map parameters) {
183 ParameterNameAware parameterNameAware = (action instanceof ParameterNameAware)
184 ? (ParameterNameAware) action : null;
185
186 Map params = null;
187 if( ordered ) {
188 params = new TreeMap(getOrderedComparator());
189 params.putAll(parameters);
190 } else {
191 params = new TreeMap(parameters);
192 }
193
194 for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) {
195 Map.Entry entry = (Map.Entry) iterator.next();
196 String name = entry.getKey().toString();
197
198 boolean acceptableName = acceptableName(name)
199 && (parameterNameAware == null
200 || parameterNameAware.acceptableParameterName(name));
201
202 if (acceptableName) {
203 Object value = entry.getValue();
204 try {
205 stack.setValue(name, value);
206 } catch (RuntimeException e) {
207 if (devMode) {
208 String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{
209 e.getMessage()
210 });
211 LOG.error(developerNotification);
212 if (action instanceof ValidationAware) {
213 ((ValidationAware) action).addActionMessage(developerNotification);
214 }
215 } else {
216 LOG.error("ParametersInterceptor - [setParameters]: Unexpected Exception caught setting '"+name+"' on '"+action.getClass()+": " + e.getMessage());
217 }
218 }
219 }
220 }
221 }
222
223 /**
224 * Gets an instance of the comparator to use for the ordered sorting. Override this
225 * method to customize the ordering of the parameters as they are set to the
226 * action.
227 *
228 * @return A comparator to sort the parameters
229 */
230 protected Comparator getOrderedComparator() {
231 return rbCollator;
232 }
233
234 private String getParameterLogMap(Map parameters) {
235 if (parameters == null) {
236 return "NONE";
237 }
238
239 StringBuffer logEntry = new StringBuffer();
240 for (Iterator paramIter = parameters.entrySet().iterator(); paramIter.hasNext();) {
241 Map.Entry entry = (Map.Entry) paramIter.next();
242 logEntry.append(String.valueOf(entry.getKey()));
243 logEntry.append(" => ");
244 if (entry.getValue() instanceof Object[]) {
245 Object[] valueArray = (Object[]) entry.getValue();
246 logEntry.append("[ ");
247 for (int indexA = 0; indexA < (valueArray.length - 1); indexA++) {
248 Object valueAtIndex = valueArray[indexA];
249 logEntry.append(valueAtIndex);
250 logEntry.append(String.valueOf(valueAtIndex));
251 logEntry.append(", ");
252 }
253 logEntry.append(String.valueOf(valueArray[valueArray.length - 1]));
254 logEntry.append(" ] ");
255 } else {
256 logEntry.append(String.valueOf(entry.getValue()));
257 }
258 }
259
260 return logEntry.toString();
261 }
262
263
264 protected boolean acceptableName(String name) {
265 if (name.indexOf('=') != -1 || name.indexOf(',') != -1 || name.indexOf('#') != -1
266 || name.indexOf(':') != -1 || isExcluded(name)) {
267 return false;
268 } else {
269 return true;
270 }
271 }
272
273 protected boolean isExcluded(String paramName) {
274 if (!this.excludeParams.isEmpty()) {
275 for (Pattern pattern : excludeParams) {
276 Matcher matcher = pattern.matcher(paramName);
277 if (matcher.matches()) {
278 return true;
279 }
280 }
281 }
282 return false;
283 }
284
285 /**
286 * Whether to order the parameters or not
287 *
288 * @return True to order
289 */
290 public boolean isOrdered() {
291 return ordered;
292 }
293
294 /**
295 * Set whether to order the parameters by object depth or not
296 *
297 * @param ordered True to order them
298 */
299 public void setOrdered(boolean ordered) {
300 this.ordered = ordered;
301 }
302
303 /**
304 * Gets a set of regular expressions of parameters to remove
305 * from the parameter map
306 *
307 * @return A set of compiled regular expression patterns
308 */
309 protected Set getExcludeParamsSet() {
310 return excludeParams;
311 }
312
313 /**
314 * Sets a comma-delimited list of regular expressions to match
315 * parameters that should be removed from the parameter map.
316 *
317 * @param commaDelim A comma-delimited list of regular expressions
318 */
319 public void setExcludeParams(String commaDelim) {
320 Collection<String> excludePatterns = asCollection(commaDelim);
321 if (excludePatterns != null) {
322 excludeParams = new HashSet<Pattern>();
323 for (String pattern : excludePatterns) {
324 excludeParams.add(Pattern.compile(pattern));
325 }
326 }
327 }
328
329 /**
330 * Return a collection from the comma delimited String.
331 *
332 * @param commaDelim the comma delimited String.
333 * @return A collection from the comma delimited String. Returns <tt>null</tt> if the string is empty.
334 */
335 private Collection asCollection(String commaDelim) {
336 if (commaDelim == null || commaDelim.trim().length() == 0) {
337 return null;
338 }
339 return TextParseUtil.commaDelimitedStringToSet(commaDelim);
340 }
341
342 }