1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <!-- 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-->
128 * <action name="someAction" class="com.examples.SomeAction">
129 * <interceptor-ref name="basicStack"/>
130 * <interceptor-ref name="hibernate"/>
131 * <interceptor-ref name="scope">
132 * <param name="session">filter,orderBy</param>
133 * <param name="autoCreateSession">true</param>
134 * </interceptor-ref>
135 * <result name="success">good_result.ftl</result>
136 * </action>
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
328
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
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
365
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
415
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 }