spring-webflow icon indicating copy to clipboard operation
spring-webflow copied to clipboard

Custom binding property enhancement [SWF-1065]

Open spring-operator opened this issue 15 years ago • 6 comments

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

spring-operator avatar Mar 15 '09 21:03 spring-operator

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>

spring-operator avatar Mar 15 '09 21:03 spring-operator

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.

spring-operator avatar Aug 18 '09 19:08 spring-operator

eli willaert commented

This issue should be merged with #273

spring-operator avatar Jun 08 '10 18:06 spring-operator

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.

spring-operator avatar Jun 09 '10 02:06 spring-operator

Rossen Stoyanchev commented

Also linking in #427, which made it easier to extend methods in AbstractMvcView.java.

spring-operator avatar Jun 09 '10 02:06 spring-operator

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 + "'";
        }
    }
}

spring-operator avatar Apr 26 '12 15:04 spring-operator