ScopeInterceptor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionProxy;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.struts2.interceptor.PreResultListener;
import org.apache.struts2.util.ValueStack;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.SessionMap;
import java.io.Serial;
import java.io.Serializable;
import java.util.IdentityHashMap;
import java.util.Map;
/**
* <!-- START SNIPPET: description -->
* <p>
* This is designed to solve a few simple issues related to wizard-like functionality in Struts. One of those issues is
* that some applications have a application-wide parameters commonly used, such <i>pageLen</i> (used for records per
* page). Rather than requiring that each action check if such parameters are supplied, this interceptor can look for
* specified parameters and pull them out of the session.
* </p>
*
* <p>This works by setting listed properties at action start with values from session/application attributes keyed
* after the action's class, the action's name, or any supplied key. After action is executed all the listed properties
* are taken back and put in session or application context.
* </p>
*
* <p>To make sure that each execution of the action is consistent it makes use of session-level locking. This way it
* guarantees that each action execution is atomic at the session level. It doesn't guarantee application level
* consistency however there has yet to be enough reasons to do so. Application level consistency would also be a big
* performance overkill.
* </p>
*
* <p>Note that this interceptor takes a snapshot of action properties just before result is presented (using a {@link
* PreResultListener}), rather than after action is invoked. There is a reason for that: At this moment we know that
* action's state is "complete" as it's values may depend on the rest of the stack and specifically - on the values of
* nested interceptors.
* </p>
*
* <!-- END SNIPPET: description -->
*
* <p><u>Interceptor parameters:</u></p>
*
* <!-- START SNIPPET: parameters -->
*
* <ul>
*
* <li>session - a list of action properties to be bound to session scope</li>
*
* <li>application - a list of action properties to be bound to application scope</li>
*
* <li>key - a session/application attribute key prefix, can contain following values:
*
* <ul>
*
* <li>CLASS - that creates a unique key prefix based on action namespace and action class, it's a default value</li>
*
* <li>ACTION - creates a unique key prefix based on action namespace and action name</li>
*
* <li>any other value is taken literally as key prefix</li>
*
* </ul>
* </li>
* <li>type - with one of the following
*
* <ul>
*
* <li>start - means it's a start action of the wizard-like action sequence and all session scoped properties are reset
* to their defaults</li>
*
* <li>end - means that session scoped properties are removed from session after action is run</li>
*
* <li>any other value throws IllegalArgumentException</li>
*
* </ul>
* </li>
*
* <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
* scope values, note that it is similar to type="start" and in fact it does the same, but in our team it is sometimes
* semantically preferred. We use session scope in two patterns - sometimes there are wizard-like action sequences that
* have start and end, and sometimes we just want simply reset current session values.</li>
*
* <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>
*
* <li>autoCreateSession - boolean value, sets if the session should be automatically created.</li>
* </ul>
*
* <!-- END SNIPPET: parameters -->
*
* <p><u>Extending the interceptor:</u></p>
*
* <!-- START SNIPPET: extending -->
*
* <p>There are no know extension points for this interceptor.</p>
*
* <!-- END SNIPPET: extending -->
*
* <p><u>Example code:</u></p>
*
* <pre>
* <!-- START SNIPPET: example -->
* <!-- As the filter and orderBy parameters are common for all my browse-type actions,
* you can move control to the scope interceptor. In the session parameter you can list
* action properties that are going to be automatically managed over session. You can
* do the same for application-scoped variables-->
* <action name="someAction" class="com.examples.SomeAction">
* <interceptor-ref name="basicStack"/>
* <interceptor-ref name="hibernate"/>
* <interceptor-ref name="scope">
* <param name="session">filter,orderBy</param>
* <param name="autoCreateSession">true</param>
* </interceptor-ref>
* <result name="success">good_result.ftl</result>
* </action>
* <!-- END SNIPPET: example -->
* </pre>
*
*/
public class ScopeInterceptor extends AbstractInterceptor implements PreResultListener {
@Serial
private static final long serialVersionUID = 9120762699600054395L;
private static final Logger LOG = LogManager.getLogger(ScopeInterceptor.class);
private String[] application = null;
private String[] session = null;
private String key;
private String type = null;
private boolean autoCreateSession = true;
private String sessionReset = "session.reset";
private boolean reset = false;
/**
* Sets a list of application scoped properties
*
* @param s A comma-delimited list
*/
public void setApplication(String s) {
if (s != null) {
application = s.split(" *, *");
}
}
/**
* Sets a list of session scoped properties
*
* @param s A comma-delimited list
*/
public void setSession(String s) {
if (s != null) {
session = s.split(" *, *");
}
}
/**
* Sets if the session should be automatically created
*
* @param value True if it should be created
*/
public void setAutoCreateSession(String value) {
if (StringUtils.isNotBlank(value)) {
this.autoCreateSession = BooleanUtils.toBoolean(value);
}
}
private String getKey(ActionInvocation invocation) {
ActionProxy proxy = invocation.getProxy();
if (key == null || "CLASS".equals(key)) {
return "struts.ScopeInterceptor:" + proxy.getAction().getClass();
} else if ("ACTION".equals(key)) {
return "struts.ScopeInterceptor:" + proxy.getNamespace() + ":" + proxy.getActionName();
}
return key;
}
/**
* The constructor
*/
public ScopeInterceptor() {
super();
}
// Since 2.0.7. Avoid null references on session serialization (WW-1803).
private static class NULLClass implements Serializable {
public String toString() {
return "NULL";
}
public int hashCode() {
return 1; // All instances of this class are equivalent
}
public boolean equals(Object obj) {
return obj == null || (obj instanceof NULLClass);
}
}
private static final Object NULL = new NULLClass();
private static Object nullConvert(Object o) {
if (o == null) {
return NULL;
}
if (o == NULL || NULL.equals(o)) {
return null;
}
return o;
}
private static final Map<Object, Object> locks = new IdentityHashMap<>();
static void lock(Object o, ActionInvocation invocation) throws Exception {
synchronized (o) {
int count = 3;
Object previous;
while ((previous = locks.get(o)) != null) {
if (previous == invocation) {
return;
}
if (count-- <= 0) {
locks.remove(o);
o.notify();
throw new StrutsException("Deadlock in session lock");
}
o.wait(10000);
}
locks.put(o, invocation);
}
}
static void unlock(Object o) {
synchronized (o) {
locks.remove(o);
o.notify();
}
}
protected void after(ActionInvocation invocation, String result) throws Exception {
Map<String, Object> session = ActionContext.getContext().getSession();
if ( session != null) {
unlock(session);
}
}
protected void before(ActionInvocation invocation) throws Exception {
invocation.addPreResultListener(this);
Map<String, Object> session = ActionContext.getContext().getSession();
if (session == null && autoCreateSession) {
session = new SessionMap(ServletActionContext.getRequest());
ActionContext.getContext().withSession(session);
}
if ( session != null) {
lock(session, invocation);
}
String key = getKey(invocation);
Map<String, Object> application = ActionContext.getContext().getApplication();
final ValueStack stack = ActionContext.getContext().getValueStack();
LOG.debug("scope interceptor before");
if (this.application != null)
for (String string : this.application) {
Object attribute = application.get(key + string);
if (attribute != null) {
LOG.debug("Application scoped variable set {} = {}", string, String.valueOf(attribute));
stack.setValue(string, nullConvert(attribute));
}
}
if (ActionContext.getContext().getParameters().get(sessionReset).isDefined()) {
return;
}
if (reset) {
return;
}
if (session == null) {
LOG.debug("No HttpSession created... Cannot set session scoped variables");
return;
}
if (this.session != null && (!"start".equals(type))) {
for (String string : this.session) {
Object attribute = session.get(key + string);
if (attribute != null) {
LOG.debug("Session scoped variable set {} = {}", string, String.valueOf(attribute));
stack.setValue(string, nullConvert(attribute));
}
}
}
}
public void setKey(String key) {
this.key = key;
}
/* (non-Javadoc)
* @see org.apache.struts2.interceptor.PreResultListener#beforeResult(org.apache.struts2.ActionInvocation, java.lang.String)
*/
public void beforeResult(ActionInvocation invocation, String resultCode) {
String key = getKey(invocation);
Map<String, Object> application = ActionContext.getContext().getApplication();
final ValueStack stack = ActionContext.getContext().getValueStack();
if (this.application != null)
for (String string : this.application) {
Object value = stack.findValue(string);
LOG.debug("Application scoped variable saved {} = {}", string, String.valueOf(value));
//if( value != null)
application.put(key + string, nullConvert(value));
}
boolean ends = "end".equals(type);
Map<String, Object> session = ActionContext.getContext().getSession();
if (session != null) {
if (this.session != null) {
for (String string : this.session) {
if (ends) {
session.remove(key + string);
} else {
Object value = stack.findValue(string);
LOG.debug("Session scoped variable saved {} = {}", string, String.valueOf(value));
// Null value should be scoped too
//if( value != null)
session.put(key + string, nullConvert(value));
}
}
}
unlock(session);
} else {
LOG.debug("No HttpSession created... Cannot save session scoped variables.");
}
LOG.debug("scope interceptor after (before result)");
}
/**
* @return The type of scope operation, "start" or "end"
*/
public String getType() {
return type;
}
/**
* Sets the type of scope operation
*
* @param type Either "start" or "end"
*/
public void setType(String type) {
type = type.toLowerCase();
if ("start".equals(type) || "end".equals(type)) {
this.type = type;
} else {
throw new IllegalArgumentException("Only start or end are allowed arguments for type");
}
}
/**
* @return Gets the session reset parameter name
*/
public String getSessionReset() {
return sessionReset;
}
/**
* @param sessionReset The session reset parameter name
*/
public void setSessionReset(String sessionReset) {
this.sessionReset = sessionReset;
}
/* (non-Javadoc)
* @see org.apache.struts2.interceptor.Interceptor#intercept(org.apache.struts2.ActionInvocation)
*/
public String intercept(ActionInvocation invocation) throws Exception {
String result;
Map<String, Object> session = ActionContext.getContext().getSession();
before(invocation);
try {
result = invocation.invoke();
after(invocation, result);
} finally {
if (session != null) {
unlock(session);
}
}
return result;
}
/**
* @return True if the scope is reset
*/
public boolean isReset() {
return reset;
}
/**
* @param reset True if the scope should be reset
*/
public void setReset(boolean reset) {
this.reset = reset;
}
}