Here is the working code I have so far... Please comment.
InjectionAction
Code:
package org.springframework.web.flow.action;
import java.util.Iterator;
import java.util.List;
import org.springframework.web.flow.Event;
import org.springframework.web.flow.RequestContext;
/**
* Action that implements logic calling injectors to inject objects into scope
*
* @author Keith Garry Boyce
*/
public class InjectionAction extends AbstractAction {
private List injectors;
/**
* @param injectors The injectors to set.
*/
public void setInjectors(List injectors) {
this.injectors = injectors;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AbstractAction#doExecuteAction(org.springframework.web.flow.RequestContext)
*/
protected Event doExecuteAction(RequestContext context) throws Exception {
Event outcome = success();
for (Iterator iter = injectors.iterator(); iter.hasNext();) {
ContextInjector injector = (ContextInjector) iter.next();
Event injectorStatus = injector.bindAndValidate(context);
if (! injectorStatus.toString().equals(outcome.toString())) {
outcome = injectorStatus;
//TODO: decide if to continue with other injectors.
break;
}
}
return outcome;
}
}
ContextInjector
Code:
package org.springframework.web.flow.action;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.validation.DataBinder;
import org.springframework.web.flow.Event;
import org.springframework.web.flow.RequestContext;
import org.springframework.web.flow.execution.portlet.PortletRequestEvent;
import org.springframework.web.flow.execution.servlet.HttpServletRequestEvent;
/**
* Multi-action that implements logic dealing with input forms: It extends FormAction to provide
* binding using AttributeResolvers
*
* It works as follows:
* <ul>
* <li>Extends from FormAction</li>
* <li>bindAndValidate of FormAction works as usual except that:
* <ul>
* <li>property resolution is done with attributeResolvers instead of just using request
* parameters</li>
* <li>These attributeResolvers support retrieval from nested properties</li>
* <li>it has the ability to populate nested properties on objects already in scope</li>
* <li>properties can be assigned from constant injected value instead of attributeResolvers.
* Value can be defined at the ContextInjector level in which case properties are resolved
* from object if property itself doesn't have a value assigned to it rather than an
* attribute resolver.</li>
* <li>An AttributeResolver can be assigned at ContextInjector level to populate the object itself.
* This would allow developers to create a AttributeResolver that accepts a service layer method
* as an injection returning the object that could then be used to populate scope object. So you
* could basically set up the entire view without a line of code.</li>
* <li>If property resolver is not an attributeResolver and it is a constant value instead.
* Property is automatically set to value.</li>
* <ul>
* </li>
* <li>for each property in class find a property resolver in following precedence
* <ul>
* <li>an explicit PropertyResolver in propertyResolvers</li>
* <li>a default PropertyResolver (which can be injected) if none specified and
* defaultPopulation is true</li>
* <ul>
* </li>
* <li>for each property instantiate PropertyResolver and
* <ul>
* <li>inject defaultAttributeSourceChain if not already specified for the property
* resolver</li>
* <li>inject property name as "to"</li>
* <ul>
* </li>
* <li>Once instantiated object has been populated, validate if necessary and then set
* formObjectName to point to object in the scope formObjectScopeAsString specified</li>
* <li>If formObjectName in the form "phoneBook.addresses" and addresses is a collection then
* find phoneBook in the scope specified by formObjectScopeAsString and invoke add method to
* insert newly populated object in collection.</li>
*
* @author Keith Garry Boyce
*/
public class ContextInjector extends FormAction {
private String defaultAttributeSourceChain;
private Map attributeResolvers = new HashMap();
private AttributeResolver defaultAttributeResolver;
private boolean defaultPopulation = false;
private Object value;
private AttributeResolver objectAttributeResolver;
/**
* @param attributeResolver The attributeResolver to set.
*/
public void setObjectAttributeResolver(AttributeResolver objectAttributeResolver) {
this.objectAttributeResolver = objectAttributeResolver;
}
/**
* @param value The value to set.
*/
public void setValue(Object value) {
this.value = value;
}
/**
* @param defaultAttributeResolver The defaultAttributeResolver to set.
*/
public void setDefaultAttributeResolver(
AttributeResolver defaultAttributeResolver) {
this.defaultAttributeResolver = defaultAttributeResolver;
}
/**
* @param attributeResolvers The attributeResolvers to set.
*/
public void setAttributeResolvers(Map attributeResolvers) {
this.attributeResolvers = attributeResolvers;
}
/**
* @param defaultAttributeSourceChain The defaultAttributeSourceChain to set.
*/
public void setDefaultAttributeSourceChain(
String defaultAttributeSourceChain) {
this.defaultAttributeSourceChain = defaultAttributeSourceChain;
}
/**
* @param defaultPopulation The defaultPopulation to set.
*/
public void setDefaultPopulation(boolean defaultPopulation) {
this.defaultPopulation = defaultPopulation;
}
public Object getObject(String objectName, RequestContext context) {
Object attributeResolverObj = attributeResolvers.get(objectName);
if (attributeResolverObj != null) {
if (attributeResolverObj instanceof AttributeResolver) {
AttributeResolver attributeResolver = (AttributeResolver) attributeResolvers.get(objectName);
if (! attributeResolver.isAttributeSourceChainDefined()) {
attributeResolver.setAttributeSourceChain(defaultAttributeSourceChain);
}
return attributeResolver.getObject(objectName,context);
} else {
// instead of using attribute resolver you can set property to constant value
return attributeResolverObj;
}
} else if (defaultPopulation) {
if (defaultAttributeResolver == null) {
if (context.getOriginatingEvent() instanceof PortletRequestEvent) {
defaultAttributeResolver = new PortletAttributeResolverImpl();
} else if (context.getOriginatingEvent() instanceof HttpServletRequestEvent) {
defaultAttributeResolver = new ServletAttributeResolverImpl();
} else {
//TODO: decide what to do if attribute resolver not assigned
throw new RuntimeException("Attribute resolver not assigned");
}
defaultAttributeResolver.setAttributeSourceChain(defaultAttributeSourceChain);
}
return defaultAttributeResolver.getObject(objectName,context);
} else {
// if you don't have a attribute resolver for property and you don't want default population to occur then return null
// as request.getAttribute would do
return null;
}
}
protected Event bindAndValidateInternal(RequestContext context, DataBinder binder) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Binding allowed matching event parameters to object '" + binder.getObjectName()
+ "', details='" + binder.getTarget() + "'");
}
// this is an addition to code in FormAction
// the only possible fields to bind are those in object itself so use reflection to determine fields
// in formbean associated with binder.
MutablePropertyValues propertyValues = new MutablePropertyValues();
PropertyDescriptor[] propertyDescriptors = new BeanWrapperImpl(binder.getTarget()).getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; i++) {
PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
if (propertyDescriptor.getWriteMethod() != null) {
Object resolvedObject = null;
//TODO: decide what precedence between
// 1) object has value
// 2) object has AttributeResolver
// 3) property has value or AttributeResolver
// if value is defined get property values from it if possible
if (value != null) {
BeanWrapper wrappedValue = new BeanWrapperImpl(value);
resolvedObject = wrappedValue.getPropertyValue(propertyDescriptor.getName());
}
if (objectAttributeResolver != null) {
if (! objectAttributeResolver.isAttributeSourceChainDefined()) {
objectAttributeResolver.setAttributeSourceChain(defaultAttributeSourceChain);
}
resolvedObject = objectAttributeResolver.getObject(getFormObjectName(),context);
}
if (resolvedObject == null) {
resolvedObject = getObject(propertyDescriptor.getName(),context);
}
PropertyValue pv = new PropertyValue(propertyDescriptor.getName(),resolvedObject);
propertyValues.addPropertyValue(pv);
}
}
//TODO: binder needs to smart enough to address nested properties
binder.bind(propertyValues);
// this is end of addition of FormAction code
// this is what used to be in FormAction code that is replaced by above.
//binder.bind(new MutablePropertyValues(context.getLastEvent().getParameters()));
onBind(context, binder.getTarget(), binder.getErrors());
if (logger.isDebugEnabled()) {
logger.debug("Binding completed for object '" + binder.getObjectName() + "', details='"
+ binder.getTarget() + "'");
}
if (getValidator() != null && isValidateOnBinding() && validationEnabled(context)) {
validate(context, binder.getTarget(), binder.getErrors());
}
return onBindAndValidate(context, binder.getTarget(), binder.getErrors());
}
}
AttributeResolver
Code:
package org.springframework.web.flow.action;
import org.springframework.web.flow.RequestContext;
/**
* Interface to be implemented by beans which act as Attribute Resolvers.
*
* @author Keith Garry Boyce
*/
public interface AttributeResolver {
/**
* Retreives object named with objectName using context and resolving using attribute source chain
*
* @param objectName name of object to find
* @param context context in which to find object
* @return found object or null if object is not found
*/
Object getObject(String objectName, RequestContext context);
/**
* Set attribute source chain
*
* @param attributeSourceChain
*/
void setAttributeSourceChain(String attributeSourceChain);
/**
* Whether this Attribute Source Chain is defined
* @return whether this Attribute Source Chain is defined
*/
boolean isAttributeSourceChainDefined();
}
AbstractAttributeResolverImpl
Code:
package org.springframework.web.flow.action;
import org.springframework.util.StringUtils;
import org.springframework.web.flow.RequestContext;
/**
* Base attribute resolver implementation that for each scope specified in attributeSourceChain,
* search in that scope for "from" name or "to" name if "from" not specified.
*
* @author Keith Garry Boyce
*/
public abstract class AbstractAttributeResolverImpl implements AttributeResolver {
private String attributeSourceChain = "parameter,request,flow,session,context";
private String fromObject;
protected static final Object SESSION = "session";
protected static final Object REQUEST = "request";
protected static final String FLOW = "flow";
protected static final Object PARAMETER = "parameter";
protected static final Object CONTEXT = "context";
/**
* @param fromObject The fromObject to set.
*/
public void setFromObject(String fromObject) {
this.fromObject = fromObject;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AttributeResolver#setAttributeSourceChain(java.lang.String)
*/
public void setAttributeSourceChain(String attributeSourceChain) {
this.attributeSourceChain = attributeSourceChain;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AttributeResolver#isAttributeSourceChainDefined()
*/
public boolean isAttributeSourceChainDefined() {
return attributeSourceChain != null;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AttributeResolver#getObject(java.lang.String, org.springframework.web.flow.RequestContext)
*/
public Object getObject(String toObject, RequestContext context) {
String objectToFind = (fromObject == null ? toObject : fromObject);
//TODO: handle nested properties in objectToFind
String[] attributeSourceChainSet = StringUtils.commaDelimitedListToStringArray(attributeSourceChain);
for (int i = 0; i < attributeSourceChainSet.length; i++) {
String attributeSource = attributeSourceChainSet[i];
if (isSupported(attributeSource)) {
Object obj = findObject(context,objectToFind,attributeSource);
//if attribute resolver cannot resolve object continue to check in other scopes
if (obj != null) {
return obj;
}
}
}
// if object cannot be found in any scope then return null just like request.getAttribute would do.
return null;
}
/**
* Subclasses must override the following method to provide the implementation by which
* the object is looked up in the context
*
* <p>
* This implementation does nothing
* @param context The context in which to look
* @param objectToFind The object to find
* @param attributeSource The attribute source
* @return
*/
protected abstract Object findObject(RequestContext context, String objectToFind,String attributeSource);
/**
* Whether implementation supports the specified attribute source
* @param attributeSource
* @return whether implementation supports the specified attribute source
*/
protected abstract boolean isSupported(String attributeSource);
}
ServletAttributeResolverImpl
Code:
package org.springframework.web.flow.action;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.flow.RequestContext;
import org.springframework.web.flow.execution.servlet.HttpServletRequestEvent;
/**
* Servlet implementation of AbstractAttributeResolverImpl
*
* @author Keith Garry Boyce
*/
public class ServletAttributeResolverImpl extends AbstractAttributeResolverImpl {
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AbstractAttributeResolverImpl#findObject(org.springframework.web.flow.RequestContext, java.lang.String, java.lang.String)
*/
protected Object findObject(RequestContext context,String objectToFind,String attributeSource) {
HttpServletRequestEvent event = (HttpServletRequestEvent) context.getOriginatingEvent();
//TODO: should use typesafe enum from ScopeType
Object obj;
if (attributeSource.equals(SESSION)) {
obj = event.getRequest().getSession().getAttribute(objectToFind);
} else if (attributeSource.equals(REQUEST)) {
obj = context.getRequestScope().getAttribute(objectToFind);
} else if (attributeSource.equals(FLOW)) {
obj = context.getFlowScope().getAttribute(objectToFind);
} else if (attributeSource.equals(PARAMETER)) {
obj = event.getParameter(objectToFind);
} else if (attributeSource.equals(CONTEXT)) {
obj = WebApplicationContextUtils.getWebApplicationContext(event.getRequest().getSession().getServletContext()).getBean(objectToFind);
} else {
throw new RuntimeException("This should not happen because this method should only be called with supported attributeSource");
}
return obj;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AbstractAttributeResolverImpl#isSupported(java.lang.String)
*/
protected boolean isSupported(String attributeSource) {
if ( attributeSource.equals(SESSION)
|| attributeSource.equals(REQUEST)
|| attributeSource.equals(FLOW)
|| attributeSource.equals(PARAMETER)
|| attributeSource.equals(CONTEXT)) {
return true;
} else {
return false;
}
}
}
PortletAttributeResolverImpl
Code:
package org.springframework.web.flow.action;
import org.springframework.web.flow.RequestContext;
import org.springframework.web.flow.execution.portlet.PortletRequestEvent;
import org.springframework.web.portlet.context.support.PortletWebApplicationContextUtils;
/**
* Portlet implementation of AbstractAttributeResolverImpl
*
* @author Keith Garry Boyce
*/
public class PortletAttributeResolverImpl extends AbstractAttributeResolverImpl {
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AbstractAttributeResolverImpl#findObject(org.springframework.web.flow.RequestContext, java.lang.String, java.lang.String)
*/
protected Object findObject(RequestContext context,String objectToFind,String attributeSource) {
PortletRequestEvent event = (PortletRequestEvent) context.getOriginatingEvent();
//TODO: should use typesafe enum from ScopeType
Object obj;
if (attributeSource.equals(SESSION)) {
obj = event.getRequest().getPortletSession().getAttribute(objectToFind);
} else if (attributeSource.equals(REQUEST)) {
obj = context.getRequestScope().getAttribute(objectToFind);
} else if (attributeSource.equals(FLOW)) {
obj = context.getFlowScope().getAttribute(objectToFind);
} else if (attributeSource.equals(PARAMETER)) {
obj = event.getParameter(objectToFind);
} else if (attributeSource.equals(CONTEXT)) {
obj = PortletWebApplicationContextUtils.getWebApplicationContext(event.getRequest().getPortletSession().getPortletContext()).getBean(objectToFind);
} else {
throw new RuntimeException("TODO: What should happen if scope is invalid");
}
return obj;
}
/* (non-Javadoc)
* @see org.springframework.web.flow.action.AbstractAttributeResolverImpl#isSupported(java.lang.String)
*/
protected boolean isSupported(String attributeSource) {
if ( attributeSource.equals(SESSION)
|| attributeSource.equals(REQUEST)
|| attributeSource.equals(FLOW)
|| attributeSource.equals(PARAMETER)
|| attributeSource.equals(CONTEXT)) {
return true;
} else {
return false;
}
}
}
Here is an very simple example how I have used it
(
look above for more complete and complicated configuration example.
This example does not even show use of attribute resolvers
so don't judge code by this example.
This is only showing one simple thing the code can do!!
):
Code:
<bean id="profile"
class="com.bcbsma.contactus.business.domain.Profile">
<property name="type">
<value>80</value>
</property>
<property name="specialty">
<value>99</value>
</property>
</bean>
<bean id="profileInjector" class="org.springframework.web.flow.action.ContextInjector">
<property name="value">
<ref local="profile"/>
</property>
<property name="formObjectName"><value>profile</value></property>
<property name="formObjectClass"><value>com.bcbsma.contactus.business.domain.Profile</value></property>
<property name="formObjectScopeAsString"><value>request</value></property>
</bean>
<bean id="contactUs.InjectionAction"
class="org.springframework.web.flow.action.InjectionAction" >
<property name="injectors">
<list>
<ref bean="profileInjector"/>
</list>
</property>
</bean>
and webflow:
Code:
<webflow id="contactUs" start-state="bindAndValidate">
<action-state id="bindAndValidate">
<action bean="contactUs.InjectionAction"/>
<transition on="success" to="pre.select.topic.action"/>
<transition on="error" to="error"/>
</action-state>
...
In action code refer to object like:
Code:
(Profile) context.getRequestScope().getAttribute("profile");
Opened JIRA on issue:
http://opensource.atlassian.com/proj...browse/SPR-945