0

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?

David M. Karr
  • 14,317
  • 20
  • 94
  • 199

1 Answers1

1

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;
        }

    }

}
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • I updated the posting with a reference to a SO posting that defines a Map in properties and injects it with @Value. That's what I'm trying to do in code. – David M. Karr Apr 03 '19 at 13:31
  • That's something completely different; Spring autowiring (e.g. `@Value`) has nothing to do with SpEL. Spring knows how to map from a String to a Map. SpEL uses `PropertyAccessor`s; Spring uses `PropertyEditors`. – Gary Russell Apr 03 '19 at 17:11
  • Ok, well, that's what I need to understand. I need to do be able to do in code what Spring does with @Value annotations reading from properties. Could you update your answer with more information? – David M. Karr Apr 03 '19 at 18:09
  • I added an example of one way to do it. – Gary Russell Apr 03 '19 at 18:50
  • I haven't started integrating this, although it looks promising, but what I don't understand is why I have to write a custom converter to do this. Spring is already doing this. Doesn't it already define a "StringToMapConverter" somewhere? I already tried obvious class names, and I didn't find one. – David M. Karr Apr 03 '19 at 19:06
  • Interesting; I didn't scroll all the way down for that question; clearly SpEL is handling it there `#{...}`, I just ran it in a debugger and the parser sees `{foo:'bar'}` as an `InlineMap`. The difference here is you are calling `setValue` with the String representation of the map. In the case of `@Value(...)` the expression therein is parsed by the SpEL parser and that is where the logic to parse the map resides. When using `exp.setValue()` a converter is used to convert the value and, no, there is no out-of-the-box `StringToMapConverter` (there are several `StringTo*`, but not one for map). – Gary Russell Apr 03 '19 at 20:13