spring-webflow
spring-webflow copied to clipboard
Custom binding property enhancement [SWF-1065]
qxo opened SWF-1065 and commented
<binder>
<binding property="parent.*"/>
<binding property="cars.*"/>
<binding property="date" converter="shortDate" required="true"/>
<binding property="name" required="true"/>
</binder>
see http://forum.springframework.org/showthread.php?t=61697&highlight=binding
just modify AbstractMvcView code as below get this feature work!
protected void addModelBindings(DefaultMapper mapper, Set parameterNames, Object model) {
Iterator it = binderConfiguration.getBindings().iterator();
while (it.hasNext()) {
Binding binding = (Binding) it.next();
String parameterName = binding.getProperty();
if (parameterNames.contains(parameterName)) {
addMapping(mapper, binding, model);
} else {
boolean matched = false;
if(parameterName.indexOf("*") != -1){
PathMatcher pathMatcher = new AntPathMatcher();
for (Iterator iterator = parameterNames.iterator(); iterator
.hasNext();) {
String pName = (String) iterator.next();
if(pathMatcher.match(parameterName, pName)){
addMapping(mapper, binding, model,pName);
matched = true;
break;
}
}
}
if (!matched && fieldMarkerPrefix != null && parameterNames.contains(fieldMarkerPrefix + parameterName)) {
addEmptyValueMapping(mapper, parameterName, model);
}
}
}
}
private void addMapping(DefaultMapper mapper, Binding binding, Object model) {
addMapping(mapper,binding,model,null);
}
private void addMapping(DefaultMapper mapper, Binding binding, Object model,String property) {
if( null == property){
property =binding.getProperty();
}
Expression source = new RequestParameterExpression(property);
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(property, parserContext);
DefaultMapping mapping = new DefaultMapping(source, target);
mapping.setRequired(binding.getRequired());
if (binding.getConverter() != null) {
Assert.notNull(conversionService,
"A ConversionService must be configured to use resolve custom converters to use during binding");
ConversionExecutor conversionExecutor = conversionService.getConversionExecutor(binding.getConverter(),
String.class, target.getValueType(model));
mapping.setTypeConverter(conversionExecutor);
}
if (logger.isDebugEnabled()) {
logger.debug("Adding mapping for parameter '" + property + "'");
}
mapper.addMapping(mapping);
}
Issue Links:
-
#273 Custom binding property enhancement
-
#427 AbstractMvcView is to difficult to extend
7 votes, 8 watchers
qxo commented
I use AntPathMatcher for the match pattern.
<binder>
<binding property="parent[*].*"/>
<binding property="cars[*].*"/>
<binding property="cars[*].id"/>
<binding property="date" converter="shortDate" required="true"/>
<binding property="name" required="true"/>
</binder>
Kengkaj S. commented
How about complement biding? If we write the following binding: <complementBinder> <binding property="name" required="true"/> <complementBinder> Other properties will be bind automatically.
Rossen Stoyanchev commented
I am linking in #273 as a related issue. I think we can close #273 as a duplicate unless there are any objections.
Rossen Stoyanchev commented
Also linking in #427, which made it easier to extend methods in AbstractMvcView.java.
Lucas Theisen commented
Perhaps an aspect version of the above workaround to avoid having to recompile a custom version of spring webflow (or is there a better way)? Here is what I came up with:
package org.anonymous.webflow;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Set;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.convert.ConversionService;
import org.springframework.binding.expression.EvaluationException;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.expression.ExpressionParser;
import org.springframework.binding.expression.ParserContext;
import org.springframework.binding.expression.support.FluentParserContext;
import org.springframework.binding.mapping.impl.DefaultMapper;
import org.springframework.binding.mapping.impl.DefaultMapping;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.webflow.core.collection.ParameterMap;
import org.springframework.webflow.engine.builder.BinderConfiguration;
import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
import org.springframework.webflow.mvc.view.AbstractMvcView;
@Aspect
public class ModelBindingsEnhancer {
private static Logger logger = LoggerFactory.getLogger( ModelBindingsEnhancer.class );
private static final Field binderConfigurationField;
private static final Field conversionServiceField;
private static final Field expressionParserField;
private static final Field fieldMarkerPrefixField;
private static final Method addEmptyValueMappingMethod;
static {
try {
binderConfigurationField = AbstractMvcView.class.getDeclaredField( "binderConfiguration" );
binderConfigurationField.setAccessible( true );
conversionServiceField = AbstractMvcView.class.getDeclaredField( "conversionServiceField" );
conversionServiceField.setAccessible( true );
expressionParserField = AbstractMvcView.class.getDeclaredField( "expressionParser" );
expressionParserField.setAccessible( true );
fieldMarkerPrefixField = AbstractMvcView.class.getDeclaredField( "fieldMarkerPrefix" );
fieldMarkerPrefixField.setAccessible( true );
addEmptyValueMappingMethod = AbstractMvcView.class.getDeclaredMethod( "addEmptyValueMapping", DefaultMapper.class, String.class, Object.class );
addEmptyValueMappingMethod.setAccessible( true );
}
catch ( Exception e ) {
throw new AspectReflectionException( "Failed to reflectively access fieldMarkerPrefix", e );
}
}
@SuppressWarnings( "unused" )
@Pointcut( "execution(* org.springframework.webflow.mvc.view.AbstractMvcView+.addModelBindings(DefaultMapper,Set,Object)) &&" +
"args(mapper,parameterNames,model) && this(self)" )
private void addModelBindings( DefaultMapper mapper, @SuppressWarnings( "rawtypes" ) Set parameterNames, Object model ) {
logger.error( "wait, what the heck... this is a pointcut, not a method!" );
}
@SuppressWarnings( "unused" )
@Pointcut( "execution(* org.springframework.webflow.mvc.view.AbstractMvcView+.addMapping(DefaultMapper,Binding,Object)) &&" +
"args(mapper,binding,model) && this(self)" )
private void addMapping( DefaultMapper mapper, Binding binding, Object model ) {
logger.error( "wait, what the heck... this is a pointcut, not a method!" );
}
@SuppressWarnings( "rawtypes" )
@Around( "org.mitre.asias.webflow.ModelBindingsEnhancer.addModelBindings(self,mapper,parameterNames,model)" )
protected void enhancedAddModelBindings( AbstractMvcView self, DefaultMapper mapper, Set parameterNames, Object model ) {
Iterator it = selfGetBinderConfiguration( self ).getBindings().iterator();
while ( it.hasNext() ) {
Binding binding = (Binding) it.next();
String parameterName = binding.getProperty();
if ( parameterNames.contains( parameterName ) ) {
addMapping( self, mapper, binding, model );
}
else {
boolean matched = false;
if ( parameterName.indexOf( "*" ) != -1 ) {
PathMatcher pathMatcher = new AntPathMatcher();
for ( Iterator iterator = parameterNames.iterator(); iterator
.hasNext(); ) {
String pName = (String) iterator.next();
if ( pathMatcher.match( parameterName, pName ) ) {
addMapping( self, mapper, binding, model, pName );
matched = true;
break;
}
}
}
String fieldMarkerPrefix = selfGetFieldMarkerPrefix( self );
if ( !matched && fieldMarkerPrefix != null && parameterNames.contains( fieldMarkerPrefix + parameterName ) ) {
selfAddEmptyValueMapping( self, mapper, parameterName, model );
}
}
}
}
@Around( "org.mitre.asias.webflow.ModelBindingsEnhancer.addModelBindings(self,mapper,binding,model)" )
private void addMapping( AbstractMvcView self, DefaultMapper mapper, Binding binding, Object model ) {
addMapping( self, mapper, binding, model, null );
}
private void addMapping( AbstractMvcView self, DefaultMapper mapper, Binding binding, Object model, String property ) {
if ( null == property ) {
property = binding.getProperty();
}
Expression source = selfGetRequestParameterExpression( self, property );
ParserContext parserContext = new FluentParserContext().evaluate( model.getClass() );
Expression target = selfGetExpressionParser( self ).parseExpression( property, parserContext );
DefaultMapping mapping = new DefaultMapping( source, target );
mapping.setRequired( binding.getRequired() );
if ( binding.getConverter() != null ) {
ConversionService conversionService = selfGetConversionService( self );
Assert.notNull( conversionService,
"A ConversionService must be configured to use resolve custom converters to use during binding" );
ConversionExecutor conversionExecutor = conversionService.getConversionExecutor( binding.getConverter(),
String.class, target.getValueType( model ) );
mapping.setTypeConverter( conversionExecutor );
}
if ( logger.isDebugEnabled() ) {
logger.debug( "Adding mapping for parameter '" + property + "'" );
}
mapper.addMapping( mapping );
}
public void selfAddEmptyValueMapping( AbstractMvcView self, DefaultMapper mapper, String parameterName, Object model ) {
try {
addEmptyValueMappingMethod.invoke( self, mapper, parameterName, model );
}
catch ( Exception e ) {
throw new AspectReflectionException( "Failed to reflectively invoke " + fieldMarkerPrefixField.getName(), e );
}
}
public ConversionService selfGetConversionService( AbstractMvcView self ) {
return get( self, conversionServiceField, ConversionService.class );
}
public BinderConfiguration selfGetBinderConfiguration( AbstractMvcView self ) {
return get( self, binderConfigurationField, BinderConfiguration.class );
}
public ExpressionParser selfGetExpressionParser( AbstractMvcView self ) {
return get( self, expressionParserField, ExpressionParser.class );
}
public String selfGetFieldMarkerPrefix( AbstractMvcView self ) {
return get( self, fieldMarkerPrefixField, String.class );
}
@SuppressWarnings( "unchecked" )
private <T> T get( AbstractMvcView self, Field field, Class<T> clazz ) {
try {
return (T)field.get( self );
}
catch ( Exception e ) {
throw new AspectReflectionException( "Failed to reflectively access " + fieldMarkerPrefixField.getName(), e );
}
}
public Expression selfGetRequestParameterExpression( AbstractMvcView self, String property ) {
return new RequestParameterExpression( property );
}
public static class AspectReflectionException extends RuntimeException {
private static final long serialVersionUID = -7531716042412123739L;
public AspectReflectionException( String message, Throwable cause ) {
super( message, cause );
}
}
private static class RequestParameterExpression implements Expression {
private String parameterName;
public RequestParameterExpression(String parameterName) {
this.parameterName = parameterName;
}
public String getExpressionString() {
return parameterName;
}
public Object getValue(Object context) throws EvaluationException {
ParameterMap parameters = (ParameterMap) context;
return parameters.asMap().get(parameterName);
}
public Class<?> getValueType(Object context) {
return String.class;
}
public void setValue(Object context, Object value) throws EvaluationException {
throw new UnsupportedOperationException("Setting request parameters is not allowed");
}
public String toString() {
return "parameter:'" + parameterName + "'";
}
}
}