Wednesday, April 2, 2014

Spring PropertyResolver

In this article, we will discuss how spring parses and resolves a string with placeholders. We will look into the algorithm and all the related scenarios.

Example

Lets suppose "Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}" is the string which needs to be parsed and resolved. If the placeholder values are:

Property resolver algorithm
Property resolver algorithm

The final parsed string should be "Buy 1 large basket of fresh Marionberries".



Test case 

In the below test case, we create the property sources and resolve the placeholders based on it.

@Test
    public void resolveMultiplePlaceholders() {
        MutablePropertySources propertySources = new MutablePropertySources();
        PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
        propertySources.addLast(new MockPropertySource()
                .withProperty("berries", "${berriesType}Berries")
                .withProperty("berriesType", "black")
                .withProperty("blackBerries", "Marionberries")
                .withProperty("container", "basket")
                .withProperty("size", "large")
                .withProperty("quantity", "one")
                .withProperty("one large", "1 large"));
        assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}"),
                equalTo("Buy 1 large basket of fresh Marionberries"));
    }

Scenarios

What are the scenarios involved?
  • A string might contain more than one place holder.
  • The place holder value itself can be composed of sub placeholders.
  • In case there is no place holder value but still there should be a way to resort to a default value.
  • In case the we don't have a default value, we should either fail it or proceed with further parsing ignoring the unresolved placeholder.

Parsing

The parsing logic and the mechanism used to resolve the placeholders is decoupled as there can be many ways to resolve a property. For example the property source might come from system environment, system properties, servlet etc.

In this article we will only concentrate on the parsing logic. Since there can be more than one place holder in a string and the resolved value itself can contain further placeholders, the parsing is done recursively.
Default values can be supplied using the ":" separator between key and value.

Below diagram contains the flow.

Property resolver algorithm
Property resolver algorithm


Based on the above mentioned example, where the string is
 "Buy ${${quantity} ${size}} ${container} of fresh ${${berries}}"


And the placeholder values:

Property resolver algorithm
Property resolver algorithm


Below diagram shows how each placeholder is parsed in sequence.

Property resolver algorithm
Property resolver algorithm



Placeholder's default value


If a placeholder's value is not specified, it can be set to a default value. 
To test this, we modify our example. In the test below, "berries" is not added to the property sources. For the test to work, we should a default value to the placeholder "berries".
This is done by using a separator. Instead of ${berries}, we now pass a default value to it using a separator ":" -> ${berries:blackberries}.
    @Test
    public void defaultPlaceholderValue() {
        MutablePropertySources propertySources = new MutablePropertySources();
        PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
        propertySources.addLast(new MockPropertySource()
                .withProperty("berriesType", "black")
                .withProperty("blackBerries", "Marionberries")
                .withProperty("container", "Basket")
                .withProperty("size", "large")
                .withProperty("quantity", "one")
                .withProperty("one large", "1 large"));
        assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries:blackBerries}}"), equalTo("Buy 1 large Basket of fresh Marionberries"));
    }
The default value itself can be another placeholder. For example, instead of ${berries:blackBerries}, we now use ${berries:${blackBerries}} where the default value is ${blackBerries}. A new placeholder is been added for "Marionberries"-> "Marion blackberry, Orgeon". The final parsed string would be "Buy 1 large Basket of fresh Marion blackberry, Orgeon".
    @Test
    public void defaultValueAsPlaceholder() {
        MutablePropertySources propertySources = new MutablePropertySources();
        PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
        propertySources.addLast(new MockPropertySource()
                .withProperty("berriesType", "black")
                .withProperty("blackBerries", "Marionberries")
                .withProperty("Marionberries", "Marion blackberry, Orgeon")
                .withProperty("container", "Basket")
                .withProperty("size", "large")
                .withProperty("quantity", "one")
                .withProperty("one large", "1 large"));
        assertThat(resolver.resolveRequiredPlaceholders("Buy ${${quantity} ${size}} ${container} of fresh ${${berries:${blackBerries}}}"), equalTo("Buy 1 large Basket of fresh Marion blackberry, Orgeon"));
    }

Missing placeholder value

By default, if a placeholder's value is not specified, the test is going to fail with an IllegalArgumentException.
    @Test
    public void missingPlaceholderError() {
        MutablePropertySources propertySources = new MutablePropertySources();
        PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
        propertySources.addLast(new MockPropertySource()
                .withProperty("two", "2"));
        try {
            resolver.resolveRequiredPlaceholders("${one} and ${two}");
            fail("should have failed as placeholder value for \"two\" is missing");
        } catch (IllegalArgumentException e) {
            assertTrue(true);
        }
    }
We may want to ignore the missing placeholder and parse rest of the string instead of aborting the parsing, in such case, we should use resolvePlaceholders instead of the stricter version resolveRequiredPlaceholders
    @Test
    public void ignoreMissingPlaceholder() {
        MutablePropertySources propertySources = new MutablePropertySources();
        PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
        propertySources.addLast(new MockPropertySource()
                .withProperty("two", "2"));
        assertThat(resolver.resolvePlaceholders("${one} and ${two}"), equalTo("${one} and 2"));
    }

No comments:

Post a Comment