View Javadoc

1   /*
2    * $Id: ScopeInterceptor.java 768855 2009-04-27 02:09:35Z wesw $
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  
22  package org.apache.struts2.interceptor;
23  
24  import java.io.Serializable;
25  import java.util.IdentityHashMap;
26  import java.util.Map;
27  
28  import org.apache.struts2.ServletActionContext;
29  import org.apache.struts2.StrutsException;
30  import org.apache.struts2.dispatcher.SessionMap;
31  
32  import com.opensymphony.xwork2.ActionContext;
33  import com.opensymphony.xwork2.ActionInvocation;
34  import com.opensymphony.xwork2.ActionProxy;
35  import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
36  import com.opensymphony.xwork2.interceptor.PreResultListener;
37  import com.opensymphony.xwork2.util.ValueStack;
38  import com.opensymphony.xwork2.util.logging.Logger;
39  import com.opensymphony.xwork2.util.logging.LoggerFactory;
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 - name of a parameter (defaults to 'session.reset') which if set, causes all session values to be reset to action's default values or application
102  * scope values, note that it is similar 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 wizard-like action sequences that
104  * have start and end, and sometimes we just want simply reset current session values.</li>
105  *
106  * <li>reset - boolean, defaults to false, if set, it has the same effect as setting all session values to be reset to action's default values or application.</li>
107  *
108  * <li>autoCreateSession - boolean value, sets if the session should be automatically created.</li>
109  * </ul>
110  *
111  * <!-- END SNIPPET: parameters -->
112  *
113  * <p/> <u>Extending the interceptor:</u>
114  *
115  * <p/>
116  *
117  * <!-- START SNIPPET: extending -->
118  *
119  * There are no know extension points for this interceptor.
120  *
121  * <!-- END SNIPPET: extending -->
122  *
123  * <p/> <u>Example code:</u>
124  *
125  * <pre>
126  * <!-- START SNIPPET: example -->
127  * &lt;!-- As the filter and orderBy parameters are common for all my browse-type actions,
128  *      you can move control to the scope interceptor. In the session parameter you can list
129  *      action properties that are going to be automatically managed over session. You can
130  *      do the same for application-scoped variables--&gt;
131  * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
132  *     &lt;interceptor-ref name="basicStack"/&gt;
133  *     &lt;interceptor-ref name="hibernate"/&gt;
134  *     &lt;interceptor-ref name="scope"&gt;
135  *         &lt;param name="session"&gt;filter,orderBy&lt;/param&gt;
136  *         &lt;param name="autoCreateSession"&gt;true&lt;/param&gt;
137  *     &lt;/interceptor-ref&gt;
138  *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
139  * &lt;/action&gt;
140  * <!-- END SNIPPET: example -->
141  * </pre>
142  *
143  */
144 public class ScopeInterceptor extends AbstractInterceptor implements PreResultListener {
145 
146     private static final long serialVersionUID = 9120762699600054395L;
147 
148     private static final Logger LOG = LoggerFactory.getLogger(ScopeInterceptor.class);
149 
150     private String[] application = null;
151     private String[] session = null;
152     private String key;
153     private String type = null;
154     private boolean autoCreateSession = true;
155     private String sessionReset = "session.reset";
156     private boolean reset = false;
157 
158     /***
159      * Sets a list of application scoped properties
160      *
161      * @param s A comma-delimited list
162      */
163     public void setApplication(String s) {
164         if (s != null) {
165             application = s.split(" *, *");
166         }
167     }
168 
169     /***
170      * Sets a list of session scoped properties
171      *
172      * @param s A comma-delimited list
173      */
174     public void setSession(String s) {
175         if (s != null) {
176             session = s.split(" *, *");
177         }
178     }
179 
180     /***
181      * Sets if the session should be automatically created
182      *
183      * @param value True if it should be created
184      */
185     public void setAutoCreateSession(String value) {
186         if (value != null && value.length() > 0) {
187             this.autoCreateSession = Boolean.valueOf(value).booleanValue();
188         }
189     }
190 
191     private String getKey(ActionInvocation invocation) {
192         ActionProxy proxy = invocation.getProxy();
193         if (key == null || "CLASS".equals(key)) {
194             return "struts.ScopeInterceptor:" + proxy.getAction().getClass();
195         } else if ("ACTION".equals(key)) {
196             return "struts.ScopeInterceptor:" + proxy.getNamespace() + ":" + proxy.getActionName();
197         }
198         return key;
199     }
200 
201     /***
202      * The constructor
203      */
204     public ScopeInterceptor() {
205         super();
206     }
207 
208     // Since 2.0.7. Avoid null references on session serialization (WW-1803).
209     private static class NULLClass implements Serializable {
210       public String toString() {
211         return "NULL";
212       }
213       public int hashCode() {
214         return 1; // All instances of this class are equivalent
215       }
216       public boolean equals(Object obj) {
217         return obj == null || (obj instanceof NULLClass);
218       }
219     }
220 
221     private static final Object NULL = new NULLClass();
222 
223     private static final Object nullConvert(Object o) {
224         if (o == null) {
225             return NULL;
226         }
227 
228         if (o == NULL || NULL.equals(o)) {
229             return null;
230         }
231 
232         return o;
233     }
234 
235     private static Map locks = new IdentityHashMap();
236 
237     static final void lock(Object o, ActionInvocation invocation) throws Exception {
238         synchronized (o) {
239             int count = 3;
240             Object previous = null;
241             while ((previous = locks.get(o)) != null) {
242                 if (previous == invocation) {
243                     return;
244                 }
245                 if (count-- <= 0) {
246                     locks.remove(o);
247                     o.notify();
248 
249                     throw new StrutsException("Deadlock in session lock");
250                 }
251                 o.wait(10000);
252             }
253             ;
254             locks.put(o, invocation);
255         }
256     }
257 
258     static final void unlock(Object o) {
259         synchronized (o) {
260             locks.remove(o);
261             o.notify();
262         }
263     }
264 
265     protected void after(ActionInvocation invocation, String result) throws Exception {
266         Map ses = ActionContext.getContext().getSession();
267         if ( ses != null) {
268             unlock(ses);
269         }
270     }
271 
272 
273     protected void before(ActionInvocation invocation) throws Exception {
274         invocation.addPreResultListener(this);
275         Map ses = ActionContext.getContext().getSession();
276         if (ses == null && autoCreateSession) {
277             ses = new SessionMap(ServletActionContext.getRequest());
278             ActionContext.getContext().setSession(ses);
279         }
280 
281         if ( ses != null) {
282             lock(ses, invocation);
283         }
284 
285         String key = getKey(invocation);
286         Map app = ActionContext.getContext().getApplication();
287         final ValueStack stack = ActionContext.getContext().getValueStack();
288 
289         if (LOG.isDebugEnabled()) {
290             LOG.debug("scope interceptor before");
291         }
292 
293         if (application != null)
294             for (int i = 0; i < application.length; i++) {
295                 String string = application[i];
296                 Object attribute = app.get(key + string);
297                 if (attribute != null) {
298                     if (LOG.isDebugEnabled()) {
299                         LOG.debug("application scoped variable set " + string + " = " + String.valueOf(attribute));
300                     }
301 
302                     stack.setValue(string, nullConvert(attribute));
303                 }
304             }
305 
306         if (ActionContext.getContext().getParameters().get(sessionReset) != null) {
307             return;
308         }
309 
310         if (reset) {
311             return;
312         }
313 
314         if (ses == null) {
315             LOG.debug("No HttpSession created... Cannot set session scoped variables");
316             return;
317         }
318 
319         if (session != null && (!"start".equals(type))) {
320             for (int i = 0; i < session.length; i++) {
321                 String string = session[i];
322                 Object attribute = ses.get(key + string);
323                 if (attribute != null) {
324                     if (LOG.isDebugEnabled()) {
325                         LOG.debug("session scoped variable set " + string + " = " + String.valueOf(attribute));
326                     }
327                     stack.setValue(string, nullConvert(attribute));
328                 }
329             }
330         }
331     }
332 
333     public void setKey(String key) {
334         this.key = key;
335     }
336 
337     /* (non-Javadoc)
338      * @see com.opensymphony.xwork2.interceptor.PreResultListener#beforeResult(com.opensymphony.xwork2.ActionInvocation, java.lang.String)
339      */
340     public void beforeResult(ActionInvocation invocation, String resultCode) {
341         String key = getKey(invocation);
342         Map app = ActionContext.getContext().getApplication();
343         final ValueStack stack = ActionContext.getContext().getValueStack();
344 
345         if (application != null)
346             for (int i = 0; i < application.length; i++) {
347                 String string = application[i];
348                 Object value = stack.findValue(string);
349                 if (LOG.isDebugEnabled()) {
350                     LOG.debug("application scoped variable saved " + string + " = " + String.valueOf(value));
351                 }
352 
353                 //if( value != null)
354                 app.put(key + string, nullConvert(value));
355             }
356 
357         boolean ends = "end".equals(type);
358 
359         Map ses = ActionContext.getContext().getSession();
360         if (ses != null) {
361 
362             if (session != null) {
363                 for (int i = 0; i < session.length; i++) {
364                     String string = session[i];
365                     if (ends) {
366                         ses.remove(key + string);
367                     } else {
368                         Object value = stack.findValue(string);
369 
370                         if (LOG.isDebugEnabled()) {
371                             LOG.debug("session scoped variable saved " + string + " = " + String.valueOf(value));
372                         }
373 
374                         // Null value should be scoped too
375                         //if( value != null)
376                         ses.put(key + string, nullConvert(value));
377                     }
378                 }
379             }
380             unlock(ses);
381         } else {
382             LOG.debug("No HttpSession created... Cannot save session scoped variables.");
383         }
384         if (LOG.isDebugEnabled()) {
385             LOG.debug("scope interceptor after (before result)");
386         }
387     }
388 
389     /***
390      * @return The type of scope operation, "start" or "end"
391      */
392     public String getType() {
393         return type;
394     }
395 
396     /***
397      * Sets the type of scope operation
398      *
399      * @param type Either "start" or "end"
400      */
401     public void setType(String type) {
402         type = type.toLowerCase();
403         if ("start".equals(type) || "end".equals(type)) {
404             this.type = type;
405         } else {
406             throw new IllegalArgumentException("Only start or end are allowed arguments for type");
407         }
408     }
409 
410     /***
411      * @return Gets the session reset parameter name
412      */
413     public String getSessionReset() {
414         return sessionReset;
415     }
416 
417     /***
418      * @param sessionReset The session reset parameter name
419      */
420     public void setSessionReset(String sessionReset) {
421         this.sessionReset = sessionReset;
422     }
423 
424     /* (non-Javadoc)
425      * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
426      */
427     public String intercept(ActionInvocation invocation) throws Exception {
428         String result = null;
429         Map ses = ActionContext.getContext().getSession();
430         before(invocation);
431         try {
432             result = invocation.invoke();
433             after(invocation, result);
434         } finally {
435             if (ses != null) {
436                 unlock(ses);
437             }
438         }
439 
440         return result;
441     }
442 
443     /***
444      * @return True if the scope is reset
445      */
446     public boolean isReset() {
447         return reset;
448     }
449 
450     /***
451      * @param reset True if the scope should be reset
452      */
453     public void setReset(boolean reset) {
454         this.reset = reset;
455     }
456 }