1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 * <!-- 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-->
123 * <action name="someAction" class="com.examples.SomeAction">
124 * <interceptor-ref name="basicStack"/>
125 * <interceptor-ref name="hibernate"/>
126 * <interceptor-ref name="scope">
127 * <param name="session">filter,orderBy</param>
128 * <param name="autoCreateSession">true</param>
129 * </interceptor-ref>
130 * <result name="success">good_result.ftl</result>
131 * </action>
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
323
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
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
360
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
410
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 }