001: /*
002: * $Id: ScopeInterceptor.java 520151 2007-03-19 22:48:16Z husted $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.interceptor;
022:
023: import java.util.IdentityHashMap;
024: import java.util.Map;
025:
026: import java.io.Serializable;
027:
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030: import org.apache.struts2.ServletActionContext;
031: import org.apache.struts2.StrutsException;
032: import org.apache.struts2.dispatcher.SessionMap;
033:
034: import com.opensymphony.xwork2.ActionContext;
035: import com.opensymphony.xwork2.ActionInvocation;
036: import com.opensymphony.xwork2.ActionProxy;
037: import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
038: import com.opensymphony.xwork2.interceptor.PreResultListener;
039: import com.opensymphony.xwork2.util.ValueStack;
040:
041: /**
042: * <!-- START SNIPPET: description -->
043: *
044: * This is designed to solve a few simple issues related to wizard-like functionality in Struts. One of those issues is
045: * that some applications have a application-wide parameters commonly used, such <i>pageLen</i> (used for records per
046: * page). Rather than requiring that each action check if such parameters are supplied, this interceptor can look for
047: * specified parameters and pull them out of the session.
048: *
049: * <p/> This works by setting listed properties at action start with values from session/application attributes keyed
050: * after the action's class, the action's name, or any supplied key. After action is executed all the listed properties
051: * are taken back and put in session or application context.
052: *
053: * <p/> To make sure that each execution of the action is consistent it makes use of session-level locking. This way it
054: * guarantees that each action execution is atomic at the session level. It doesn't guarantee application level
055: * consistency however there has yet to be enough reasons to do so. Application level consistency would also be a big
056: * performance overkill.
057: *
058: * <p/> Note that this interceptor takes a snapshot of action properties just before result is presented (using a {@link
059: * PreResultListener}), rather than after action is invoked. There is a reason for that: At this moment we know that
060: * action's state is "complete" as it's values may depend on the rest of the stack and specifically - on the values of
061: * nested interceptors.
062: *
063: * <!-- END SNIPPET: description -->
064: *
065: * <p/> <u>Interceptor parameters:</u>
066: *
067: * <!-- START SNIPPET: parameters -->
068: *
069: * <ul>
070: *
071: * <li>session - a list of action properties to be bound to session scope</li>
072: *
073: * <li>application - a list of action properties to be bound to application scope</li>
074: *
075: * <li>key - a session/application attribute key prefix, can contain following values:</li>
076: *
077: * <ul>
078: *
079: * <li>CLASS - that creates a unique key prefix based on action namespace and action class, it's a default value</li>
080: *
081: * <li>ACTION - creates a unique key prefix based on action namespace and action name</li>
082: *
083: * <li>any other value is taken literally as key prefix</li>
084: *
085: * </ul>
086: *
087: * <li>type - with one of the following</li>
088: *
089: * <ul>
090: *
091: * <li>start - means it's a start action of the wizard-like action sequence and all session scoped properties are reset
092: * to their defaults</li>
093: *
094: * <li>end - means that session scoped properties are removed from session after action is run</li>
095: *
096: * <li>any other value or no value means that it's in-the-middle action that is set with session properties before it's
097: * executed, and it's properties are put back to session after execution</li>
098: *
099: * </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
142: PreResultListener {
143:
144: private static final long serialVersionUID = 9120762699600054395L;
145:
146: private static final Log LOG = LogFactory
147: .getLog(ScopeInterceptor.class);
148:
149: private String[] application = null;
150: private String[] session = null;
151: private String key;
152: private String type = null;
153: private boolean autoCreateSession = true;
154: private String sessionReset = "session.reset";
155: private boolean reset = false;
156:
157: /**
158: * Sets a list of application scoped properties
159: *
160: * @param s A comma-delimited list
161: */
162: public void setApplication(String s) {
163: if (s != null) {
164: application = s.split(" *, *");
165: }
166: }
167:
168: /**
169: * Sets a list of session scoped properties
170: *
171: * @param s A comma-delimited list
172: */
173: public void setSession(String s) {
174: if (s != null) {
175: session = s.split(" *, *");
176: }
177: }
178:
179: /**
180: * Sets if the session should be automatically created
181: *
182: * @param value True if it should be created
183: */
184: public void setAutoCreateSession(String value) {
185: if (value != null && value.length() > 0) {
186: this .autoCreateSession = new Boolean(value).booleanValue();
187: }
188: }
189:
190: private String getKey(ActionInvocation invocation) {
191: ActionProxy proxy = invocation.getProxy();
192: if (key == null || "CLASS".equals(key)) {
193: return "struts.ScopeInterceptor:"
194: + proxy.getAction().getClass();
195: } else if ("ACTION".equals(key)) {
196: return "struts.ScopeInterceptor:" + proxy.getNamespace()
197: + ":" + proxy.getActionName();
198: }
199: return key;
200: }
201:
202: /**
203: * The constructor
204: */
205: public ScopeInterceptor() {
206: super ();
207: }
208:
209: // Since 2.0.7. Avoid null references on session serialization (WW-1803).
210: private static class NULLClass implements Serializable {
211: public String toString() {
212: return "NULL";
213: }
214:
215: public boolean equals(Object obj) {
216: return obj == null || (obj instanceof NULLClass);
217: }
218: }
219:
220: private static final Object NULL = new NULLClass();
221:
222: private static final Object nullConvert(Object o) {
223: if (o == null) {
224: return NULL;
225: }
226:
227: if (o == NULL || NULL.equals(o)) {
228: return null;
229: }
230:
231: return o;
232: }
233:
234: private static Map locks = new IdentityHashMap();
235:
236: static final void lock(Object o, ActionInvocation invocation)
237: 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(
250: "Deadlock in session lock");
251: }
252: o.wait(10000);
253: }
254: ;
255: locks.put(o, invocation);
256: }
257: }
258:
259: static final void unlock(Object o) {
260: synchronized (o) {
261: locks.remove(o);
262: o.notify();
263: }
264: }
265:
266: protected void after(ActionInvocation invocation, String result)
267: throws Exception {
268: Map ses = ActionContext.getContext().getSession();
269: if (ses != null) {
270: unlock(ses);
271: }
272: }
273:
274: protected void before(ActionInvocation invocation) throws Exception {
275: invocation.addPreResultListener(this );
276: Map ses = ActionContext.getContext().getSession();
277: if (ses == null && autoCreateSession) {
278: ses = new SessionMap(ServletActionContext.getRequest());
279: ActionContext.getContext().setSession(ses);
280: }
281:
282: if (ses != null) {
283: lock(ses, invocation);
284: }
285:
286: String key = getKey(invocation);
287: Map app = ActionContext.getContext().getApplication();
288: final ValueStack stack = ActionContext.getContext()
289: .getValueStack();
290:
291: if (LOG.isDebugEnabled()) {
292: LOG.debug("scope interceptor before");
293: }
294:
295: if (application != null)
296: for (int i = 0; i < application.length; i++) {
297: String string = application[i];
298: Object attribute = app.get(key + string);
299: if (attribute != null) {
300: if (LOG.isDebugEnabled()) {
301: LOG.debug("application scoped variable set "
302: + string + " = "
303: + String.valueOf(attribute));
304: }
305:
306: stack.setValue(string, nullConvert(attribute));
307: }
308: }
309:
310: if (ActionContext.getContext().getParameters()
311: .get(sessionReset) != null) {
312: return;
313: }
314:
315: if (reset) {
316: return;
317: }
318:
319: if (ses == null) {
320: LOG
321: .debug("No HttpSession created... Cannot set session scoped variables");
322: return;
323: }
324:
325: if (session != null && (!"start".equals(type))) {
326: for (int i = 0; i < session.length; i++) {
327: String string = session[i];
328: Object attribute = ses.get(key + string);
329: if (attribute != null) {
330: if (LOG.isDebugEnabled()) {
331: LOG.debug("session scoped variable set "
332: + string + " = "
333: + String.valueOf(attribute));
334: }
335: stack.setValue(string, nullConvert(attribute));
336: }
337: }
338: }
339: }
340:
341: public void setKey(String key) {
342: this .key = key;
343: }
344:
345: /* (non-Javadoc)
346: * @see com.opensymphony.xwork2.interceptor.PreResultListener#beforeResult(com.opensymphony.xwork2.ActionInvocation, java.lang.String)
347: */
348: public void beforeResult(ActionInvocation invocation,
349: String resultCode) {
350: String key = getKey(invocation);
351: Map app = ActionContext.getContext().getApplication();
352: final ValueStack stack = ActionContext.getContext()
353: .getValueStack();
354:
355: if (application != null)
356: for (int i = 0; i < application.length; i++) {
357: String string = application[i];
358: Object value = stack.findValue(string);
359: if (LOG.isDebugEnabled()) {
360: LOG.debug("application scoped variable saved "
361: + string + " = " + String.valueOf(value));
362: }
363:
364: //if( value != null)
365: app.put(key + string, nullConvert(value));
366: }
367:
368: boolean ends = "end".equals(type);
369:
370: Map ses = ActionContext.getContext().getSession();
371: if (ses != null) {
372:
373: if (session != null) {
374: for (int i = 0; i < session.length; i++) {
375: String string = session[i];
376: if (ends) {
377: ses.remove(key + string);
378: } else {
379: Object value = stack.findValue(string);
380:
381: if (LOG.isDebugEnabled()) {
382: LOG.debug("session scoped variable saved "
383: + string + " = "
384: + String.valueOf(value));
385: }
386:
387: // Null value should be scoped too
388: //if( value != null)
389: ses.put(key + string, nullConvert(value));
390: }
391: }
392: }
393: unlock(ses);
394: } else {
395: LOG
396: .debug("No HttpSession created... Cannot save session scoped variables.");
397: }
398: if (LOG.isDebugEnabled()) {
399: LOG.debug("scope interceptor after (before result)");
400: }
401: }
402:
403: /**
404: * @return The type of scope operation, "start" or "end"
405: */
406: public String getType() {
407: return type;
408: }
409:
410: /**
411: * Sets the type of scope operation
412: *
413: * @param type Either "start" or "end"
414: */
415: public void setType(String type) {
416: type = type.toLowerCase();
417: if ("start".equals(type) || "end".equals(type)) {
418: this .type = type;
419: } else {
420: throw new IllegalArgumentException(
421: "Only start or end are allowed arguments for type");
422: }
423: }
424:
425: /**
426: * @return Gets the session reset parameter name
427: */
428: public String getSessionReset() {
429: return sessionReset;
430: }
431:
432: /**
433: * @param sessionReset The session reset parameter name
434: */
435: public void setSessionReset(String sessionReset) {
436: this .sessionReset = sessionReset;
437: }
438:
439: /* (non-Javadoc)
440: * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
441: */
442: public String intercept(ActionInvocation invocation)
443: throws Exception {
444: String result = null;
445: Map ses = ActionContext.getContext().getSession();
446: before(invocation);
447: try {
448: result = invocation.invoke();
449: after(invocation, result);
450: } finally {
451: if (ses != null) {
452: unlock(ses);
453: }
454: }
455:
456: return result;
457: }
458:
459: /**
460: * @return True if the scope is reset
461: */
462: public boolean isReset() {
463: return reset;
464: }
465:
466: /**
467: * @param reset True if the scope should be reset
468: */
469: public void setReset(boolean reset) {
470: this.reset = reset;
471: }
472: }
|