Home » xwork-2.1.5 » com.opensymphony » xwork2 » interceptor » [javadoc | source]

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

Home » xwork-2.1.5 » com.opensymphony » xwork2 » interceptor » [javadoc | source]