View Javadoc

    1   /*
    2    * $Id: ScopeInterceptor.java 495097 2007-01-11 02:57:59Z mrdon $
    3    *
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *  http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21  package org.apache.struts2.interceptor;
    22
    23  import java.util.IdentityHashMap;
    24  import java.util.Map;
    25
    26  import java.io.Serializable;
    27
    28  import org.apache.commons.logging.Log;
    29  import org.apache.commons.logging.LogFactory;
    30  import org.apache.struts2.ServletActionContext;
    31  import org.apache.struts2.StrutsException;
    32  import org.apache.struts2.dispatcher.SessionMap;
    33
    34  import com.opensymphony.xwork2.ActionContext;
    35  import com.opensymphony.xwork2.ActionInvocation;
    36  import com.opensymphony.xwork2.ActionProxy;
    37  import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
    38  import com.opensymphony.xwork2.interceptor.PreResultListener;
    39  import com.opensymphony.xwork2.util.ValueStack;
    40
    41  /***
    42   * <!-- START SNIPPET: description -->
    43   *
    44   * This is designed to solve a few simple issues related to wizard-like functionality in Struts. One of those issues is
    45   * that some applications have a application-wide parameters commonly used, such <i>pageLen</i> (used for records per
    46   * page). Rather than requiring that each action check if such parameters are supplied, this interceptor can look for
    47   * specified parameters and pull them out of the session.
    48   *
    49   * <p/> This works by setting listed properties at action start with values from session/application attributes keyed
    50   * after the action's class, the action's name, or any supplied key. After action is executed all the listed properties
    51   * are taken back and put in session or application context.
    52   *
    53   * <p/> To make sure that each execution of the action is consistent it makes use of session-level locking. This way it
    54   * guarantees that each action execution is atomic at the session level. It doesn't guarantee application level
    55   * consistency however there has yet to be enough reasons to do so. Application level consistency would also be a big
    56   * performance overkill.
    57   *
    58   * <p/> Note that this interceptor takes a snapshot of action properties just before result is presented (using a {@link
    59   * PreResultListener}), rather than after action is invoked. There is a reason for that: At this moment we know that
    60   * action's state is "complete" as it's values may depend on the rest of the stack and specifically - on the values of
    61   * nested interceptors.
    62   *
    63   * <!-- END SNIPPET: description -->
    64   *
    65   * <p/> <u>Interceptor parameters:</u>
    66   *
    67   * <!-- START SNIPPET: parameters -->
    68   *
    69   * <ul>
    70   *
    71   * <li>session - a list of action properties to be bound to session scope</li>
    72   *
    73   * <li>application - a list of action properties to be bound to application scope</li>
    74   *
    75   * <li>key - a session/application attribute key prefix, can contain following values:</li>
    76   *
    77   * <ul>
    78   *
    79   * <li>CLASS - that creates a unique key prefix based on action namespace and action class, it's a default value</li>
    80   *
    81   * <li>ACTION - creates a unique key prefix based on action namespace and action name</li>
    82   *
    83   * <li>any other value is taken literally as key prefix</li>
    84   *
    85   * </ul>
    86   *
    87   * <li>type - with one of the following</li>
    88   *
    89   * <ul>
    90   *
    91   * <li>start - means it's a start action of the wizard-like action sequence and all session scoped properties are reset
    92   * to their defaults</li>
    93   *
    94   * <li>end - means that session scoped properties are removed from session after action is run</li>
    95   *
    96   * <li>any other value or no value means that it's in-the-middle action that is set with session properties before it's
    97   * executed, and it's properties are put back to session after execution</li>
    98   *
    99   * </ul>
    100  *
    101  * <li>sessionReset - boolean value causing all session values to be reset to action's default values or application
    102  * scope values, note that it is similliar to type="start" and in fact it does the same, but in our team it is sometimes
    103  * semantically preferred. We use session scope in two patterns - sometimes there are wizzard-like action sequences that
    104  * have start and end, and sometimes we just want simply reset current session values.</li>
    105  *
    106  * </ul>
    107  *
    108  * <!-- END SNIPPET: parameters -->
    109  *
    110  * <p/> <u>Extending the interceptor:</u>
    111  *
    112  * <p/>
    113  *
    114  * <!-- START SNIPPET: extending -->
    115  *
    116  * There are no know extension points for this interceptor.
    117  *
    118  * <!-- END SNIPPET: extending -->
    119  *
    120  * <p/> <u>Example code:</u>
    121  *
    122  * <pre>
    123  * <!-- START SNIPPET: example -->
    124  * &lt;!-- As the filter and orderBy parameters are common for all my browse-type actions,
    125  *      you can move control to the scope interceptor. In the session parameter you can list
    126  *      action properties that are going to be automatically managed over session. You can
    127  *      do the same for application-scoped variables--&gt;
    128  * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
    129  *     &lt;interceptor-ref name="basicStack"/&gt;
    130  *     &lt;interceptor-ref name="hibernate"/&gt;
    131  *     &lt;interceptor-ref name="scope"&gt;
    132  *         &lt;param name="session"&gt;filter,orderBy&lt;/param&gt;
    133  *         &lt;param name="autoCreateSession"&gt;true&lt;/param&gt;
    134  *     &lt;/interceptor-ref&gt;
    135  *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
    136  * &lt;/action&gt;
    137  * <!-- END SNIPPET: example -->
    138  * </pre>
    139  *
    140  */
    141 public class ScopeInterceptor extends AbstractInterceptor implements PreResultListener {
    142
    143     private static final long serialVersionUID = 9120762699600054395L;
    144
    145     private static final Log LOG = LogFactory.getLog(ScopeInterceptor.class);
    146
    147     private String[] application = null;
    148     private String[] session = null;
    149     private String key;
    150     private String type = null;
    151     private boolean autoCreateSession = true;
    152     private String sessionReset = "session.reset";
    153     private boolean reset = false;
    154
    155     /***
    156      * Sets a list of application scoped properties
    157      *
    158      * @param s A comma-delimited list
    159      */
    160     public void setApplication(String s) {
    161         if (s != null) {
    162             application = s.split(" *, *");
    163         }
    164     }
    165
    166     /***
    167      * Sets a list of session scoped properties
    168      *
    169      * @param s A comma-delimited list
    170      */
    171     public void setSession(String s) {
    172         if (s != null) {
    173             session = s.split(" *, *");
    174         }
    175     }
    176
    177     /***
    178      * Sets if the session should be automatically created
    179      *
    180      * @param value True if it should be created
    181      */
    182     public void setAutoCreateSession(String value) {
    183         if (value != null && value.length() > 0) {
    184             this.autoCreateSession = new Boolean(value).booleanValue();
    185         }
    186     }
    187
    188     private String getKey(ActionInvocation invocation) {
    189         ActionProxy proxy = invocation.getProxy();
    190         if (key == null || "CLASS".equals(key)) {
    191             return "struts.ScopeInterceptor:" + proxy.getAction().getClass();
    192         } else if ("ACTION".equals(key)) {
    193             return "struts.ScopeInterceptor:" + proxy.getNamespace() + ":" + proxy.getActionName();
    194         }
    195         return key;
    196     }
    197
    198     /***
    199      * The constructor
    200      */
    201     public ScopeInterceptor() {
    202         super();
    203     }
    204
    205
    206     private static final Object NULL = new Serializable() {
    207         public String toString() {
    208             return "NULL";
    209         }
    210     };
    211
    212     private static final Object nullConvert(Object o) {
    213         if (o == null) {
    214             return NULL;
    215         }
    216
    217         if (o == NULL) {
    218             return null;
    219         }
    220
    221         return o;
    222     }
    223
    224
    225     private static Map locks = new IdentityHashMap();
    226
    227     static final void lock(Object o, ActionInvocation invocation) throws Exception {
    228         synchronized (o) {
    229             int count = 3;
    230             Object previous = null;
    231             while ((previous = locks.get(o)) != null) {
    232                 if (previous == invocation) {
    233                     return;
    234                 }
    235                 if (count-- <= 0) {
    236                     locks.remove(o);
    237                     o.notify();
    238
    239                     throw new StrutsException("Deadlock in session lock");
    240                 }
    241                 o.wait(10000);
    242             }
    243             ;
    244             locks.put(o, invocation);
    245         }
    246     }
    247
    248     static final void unlock(Object o) {
    249         synchronized (o) {
    250             locks.remove(o);
    251             o.notify();
    252         }
    253     }
    254
    255     protected void after(ActionInvocation invocation, String result) throws Exception {
    256         Map ses = ActionContext.getContext().getSession();
    257         if ( ses != null) {
    258             unlock(ses);
    259         }
    260     }
    261
    262
    263     protected void before(ActionInvocation invocation) throws Exception {
    264         invocation.addPreResultListener(this);
    265         Map ses = ActionContext.getContext().getSession();
    266         if (ses == null && autoCreateSession) {
    267             ses = new SessionMap(ServletActionContext.getRequest());
    268             ActionContext.getContext().setSession(ses);
    269         }
    270
    271         if ( ses != null) {
    272             lock(ses, invocation);
    273         }
    274
    275         String key = getKey(invocation);
    276         Map app = ActionContext.getContext().getApplication();
    277         final ValueStack stack = ActionContext.getContext().getValueStack();
    278
    279         if (LOG.isDebugEnabled()) {
    280             LOG.debug("scope interceptor before");
    281         }
    282
    283         if (application != null)
    284             for (int i = 0; i < application.length; i++) {
    285                 String string = application[i];
    286                 Object attribute = app.get(key + string);
    287                 if (attribute != null) {
    288                     if (LOG.isDebugEnabled()) {
    289                         LOG.debug("application scoped variable set " + string + " = " + String.valueOf(attribute));
    290                     }
    291
    292                     stack.setValue(string, nullConvert(attribute));
    293                 }
    294             }
    295
    296         if (ActionContext.getContext().getParameters().get(sessionReset) != null) {
    297             return;
    298         }
    299
    300         if (reset) {
    301             return;
    302         }
    303
    304         if (ses == null) {
    305             LOG.debug("No HttpSession created... Cannot set session scoped variables");
    306             return;
    307         }
    308
    309         if (session != null && (!"start".equals(type))) {
    310             for (int i = 0; i < session.length; i++) {
    311                 String string = session[i];
    312                 Object attribute = ses.get(key + string);
    313                 if (attribute != null) {
    314                     if (LOG.isDebugEnabled()) {
    315                         LOG.debug("session scoped variable set " + string + " = " + String.valueOf(attribute));
    316                     }
    317                     stack.setValue(string, nullConvert(attribute));
    318                 }
    319             }
    320         }
    321     }
    322
    323     public void setKey(String key) {
    324         this.key = key;
    325     }
    326
    327     /* (non-Javadoc)
    328      * @see com.opensymphony.xwork2.interceptor.PreResultListener#beforeResult(com.opensymphony.xwork2.ActionInvocation, java.lang.String)
    329      */
    330     public void beforeResult(ActionInvocation invocation, String resultCode) {
    331         String key = getKey(invocation);
    332         Map app = ActionContext.getContext().getApplication();
    333         final ValueStack stack = ActionContext.getContext().getValueStack();
    334
    335         if (application != null)
    336             for (int i = 0; i < application.length; i++) {
    337                 String string = application[i];
    338                 Object value = stack.findValue(string);
    339                 if (LOG.isDebugEnabled()) {
    340                     LOG.debug("application scoped variable saved " + string + " = " + String.valueOf(value));
    341                 }
    342
    343                 //if( value != null)
    344                 app.put(key + string, nullConvert(value));
    345             }
    346
    347         boolean ends = "end".equals(type);
    348
    349         Map ses = ActionContext.getContext().getSession();
    350         if (ses != null) {
    351
    352             if (session != null) {
    353                 for (int i = 0; i < session.length; i++) {
    354                     String string = session[i];
    355                     if (ends) {
    356                         ses.remove(key + string);
    357                     } else {
    358                         Object value = stack.findValue(string);
    359
    360                         if (LOG.isDebugEnabled()) {
    361                             LOG.debug("session scoped variable saved " + string + " = " + String.valueOf(value));
    362                         }
    363
    364                         // Null value should be scoped too
    365                         //if( value != null)
    366                         ses.put(key + string, nullConvert(value));
    367                     }
    368                 }
    369             }
    370             unlock(ses);
    371         } else {
    372             LOG.debug("No HttpSession created... Cannot save session scoped variables.");
    373         }
    374         if (LOG.isDebugEnabled()) {
    375             LOG.debug("scope interceptor after (before result)");
    376         }
    377     }
    378
    379     /***
    380      * @return The type of scope operation, "start" or "end"
    381      */
    382     public String getType() {
    383         return type;
    384     }
    385
    386     /***
    387      * Sets the type of scope operation
    388      *
    389      * @param type Either "start" or "end"
    390      */
    391     public void setType(String type) {
    392         type = type.toLowerCase();
    393         if ("start".equals(type) || "end".equals(type)) {
    394             this.type = type;
    395         } else {
    396             throw new IllegalArgumentException("Only start or end are allowed arguments for type");
    397         }
    398     }
    399
    400     /***
    401      * @return Gets the session reset parameter name
    402      */
    403     public String getSessionReset() {
    404         return sessionReset;
    405     }
    406
    407     /***
    408      * @param sessionReset The session reset parameter name
    409      */
    410     public void setSessionReset(String sessionReset) {
    411         this.sessionReset = sessionReset;
    412     }
    413
    414     /* (non-Javadoc)
    415      * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
    416      */
    417     public String intercept(ActionInvocation invocation) throws Exception {
    418         String result = null;
    419         Map ses = ActionContext.getContext().getSession();
    420         before(invocation);
    421         try {
    422             result = invocation.invoke();
    423             after(invocation, result);
    424         } finally {
    425             if (ses != null) {
    426                 unlock(ses);
    427             }
    428         }
    429
    430         return result;
    431     }
    432
    433     /***
    434      * @return True if the scope is reset
    435      */
    436     public boolean isReset() {
    437         return reset;
    438     }
    439
    440     /***
    441      * @param reset True if the scope should be reset
    442      */
    443     public void setReset(boolean reset) {
    444         this.reset = reset;
    445     }
    446 }