1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <!-- 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-->
131 * <action name="someAction" class="com.examples.SomeAction">
132 * <interceptor-ref name="basicStack"/>
133 * <interceptor-ref name="hibernate"/>
134 * <interceptor-ref name="scope">
135 * <param name="session">filter,orderBy</param>
136 * <param name="autoCreateSession">true</param>
137 * </interceptor-ref>
138 * <result name="success">good_result.ftl</result>
139 * </action>
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
209 private static class NULLClass implements Serializable {
210 public String toString() {
211 return "NULL";
212 }
213 public int hashCode() {
214 return 1;
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
338
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
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
375
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
425
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 }