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 }