View Javadoc

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