0
votes

I'm writing some code to allow dynamic property changes using the Spring Expression Language. I pass in a bean name, property name, and expression for the new value, all strings.

This works fine for properties of type string, int, boolean, and list. I'm unable to get a map property to work. I've looked at the SPeL documentation, including examples, but I don't see anything wrong with what I'm doing. The exception I get back is not helpful.

Ignoring try/catch blocks, the basic code is just this:

ExpressionParser parser = new SpelExpressionParser();
Expression parsedPropertyNameExpression = parser.parseExpression(propertyName);
SimpleEvaluationContext evalContext = SimpleEvaluationContext.forReadWriteDataBinding().build();
Object currentValue = parsedPropertyNameExpression.getValue(evalContext, bean);
parsedPropertyNameExpression.setValue(evalContext, bean, expression);

When my "expression" is "789, 0123, 345" and the property I'm setting is a List, this works perfectly fine.

However, when I'm setting a property of type Map (""), where the expression value is "{abc:'def',ghi:'jkl'}", I get the following exception:

 org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [java.util.Map<java.lang.String, java.lang.String>]

I've tried different variations of that expression string, with basically the same result.

Update:

I noticed the following SO posting: How to inject a Map using the @Value Spring Annotation? .

One of the unaccepted answers mentions defining a Map in properties and injecting that with a @Value annotation, which I would think is using a similar mechanism. How can I do that in code?

1

1 Answers

1
votes

The exception I get back is not helpful.

No converter found capable of converting from type [java.lang.String] to type [java.util.Map]

Seems clear to me.

There is no built-in support for converting a string representation of a map to a Map object.

You can register a custom function, or use a Jackson ObjectMapper bean reference in the SpEL expression.

EDIT

Here's one way to do it (with a custom Converter using Jackson)...

public class So55485198Application {

    public static void main(String[] args) {
        Bean bean = new Bean();
        getAndSet("list", bean, "abc, def");
        getAndSet("map", bean, "{'abc':'def'}");
    }

    public static void getAndSet(String propertyName, Bean bean, String expression) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression parsedPropertyNameExpression = parser.parseExpression(propertyName);
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToMapConverter());
        SimpleEvaluationContext evalContext = SimpleEvaluationContext.forReadWriteDataBinding()
                .withConversionService(conversionService)
                .build();
        Object currentValue = parsedPropertyNameExpression.getValue(evalContext, bean);
        System.out.println("old:" + currentValue);
        parsedPropertyNameExpression.setValue(evalContext, bean, expression);
        System.out.println("new:" + parsedPropertyNameExpression.getValue(evalContext, bean));
    }

    static class StringToMapConverter implements Converter<String, Map<String, String>> {

        private static final ObjectMapper objectMapper = new ObjectMapper();

        static {
            objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        }

        @SuppressWarnings("unchecked")
        @Override
        public Map<String, String> convert(String source) {
            try {
                return this.objectMapper.readValue(source, LinkedHashMap.class);
            }
            catch (IOException e) {
                e.printStackTrace();
                throw new IllegalStateException(e);
            }
        }

    }

    static class Bean {

        private List<String> list = new ArrayList<>(Arrays.asList("foo", "bar"));

        private Map<String, String> map = new HashMap<>(Collections.singletonMap("foo", "bar"));

        public List<String> getList() {
            return this.list;
        }

        public void setList(List<String> list) {
            this.list = list;
        }

        public Map<String, String> getMap() {
            return this.map;
        }

        public void setMap(Map<String, String> map) {
            this.map = map;
        }

    }

}