Search code examples
javaspringspring-el

Using application-wide properties in SpEL (programmatically)


I need to build a small module that translates a SpEL-encoded expression into a string. This in order to throw Jexl away and access Application context.

For example, if my correctly-configured property file contains

application.name=AppTest
company.name=ACME Inc.

I would like the following path, or a string similar to it, to be translated according to properties

/path/to/#{application.name}/#{company.name}

# or $ is irrelevant for me

Normally in Spring you can inject properties into beans, and I have succeeded already. But now I want the user to enter a template string that can be translated using all the properties in the Application Context. Currently I don't need to access bean properties, but I might have in future. The above is the simplified variant of the most common scenario when defining a folder path. Little complexity is added by runtime parameters (e.g. time of day) but I am asking the question as is to work step by step.

So I have tried to understand Spring Expressions by playing with them, with the help of JUnit. I wrote the following code but can't manage to make it work

Code

final HashMap<String, Object> propertySource = new HashMap<String, Object>();
private final String FOLDER_PATTERN = "#tmp/appTest/#{company}_#{appname}/q1"; //#tmp is only token being replaced "hardcoded", not passed to Spring

@Before
public void setUp() throws Exception
{

    propertySource.put("appname", APPNAME);
    propertySource.put("company", COMPANY);
    applicationContext.getEnvironment()
                      .getPropertySources()
                      .addLast(new MapPropertySource("test", propertySource));

}

@Test
public void playWithExpression()
{
    ExpressionParser expParser = new SpelExpressionParser();
    StandardEvaluationContext stdEvaluationContext = new StandardEvaluationContext();
    stdEvaluationContext.setBeanResolver(new BeanFactoryResolver(applicationContext.getBeanFactory()));
    // stdEvaluationContext.setVariables(propertySource);

    final TemplateParserContext templateParserContext = new TemplateParserContext();

    String folderPattern = FOLDER_PATTERN.replace("#tmp", SYSTEM_TEMP_DIR);

    String realPath = expParser.parseExpression(folderPattern, templateParserContext)
                               .getValue(stdEvaluationContext, String.class);

    String calculatedPath = folderPattern.replace("#{company}", COMPANY)
                                         .replace("#{appname}", APPNAME);

    assertEquals(calculatedPath, realPath);

}

Explanation

Partially copying and pasting other examples, I:

  • Inject a custom property source to add properties only for the current JUnit test
  • Instantiate the EvaluationContext with BeanFactory as bean resolver
  • Parse the expression with the default template parser context that uses #{..} notation
  • Retrieve value of that expression using the above evaluation context. This fails

The expression is successfully instantiated but getValue throws exception

org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'company' cannot be found on null
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:220)
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:94)
    at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:81)
    at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:131)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:299)
    at org.springframework.expression.common.CompositeStringExpression.getValue(CompositeStringExpression.java:89)
    at org.springframework.expression.common.CompositeStringExpression.getValue(CompositeStringExpression.java:136)
    at it.phoenix.web.data.managers.test.FoldersManagerTemplateTests.playWithExpression(FoldersManagerTemplateTests.java:80)

Judging from the error, it seems that Spring is trying to resolve company token as a property of the root object, which is missing in this case.

If I use the #{@...} notation then Spring tries to interpret the token as a bean identifier. Not my case.

How can I resolve properties from AppContext's PropertySources in an Expression?


Solution

  • There might be a more elegant solution but

    private final String FOLDER_PATTERN = "#tmp/appTest/#{environment.getProperty('company')}"
            + "_#{environment.getProperty('appname')}/q1";  // #tmp is only token being replaced
                                                            // "hardcoded", not passed to Spring
    

    with

        String realPath = expParser.parseExpression(folderPattern, templateParserContext)
                .getValue(stdEvaluationContext, applicationContext, String.class);
    

    works (i.e. use the application context as the root object in the evaluation).

    EDIT

    Slightly better...

    private final String FOLDER_PATTERN = "#tmp/appTest/#{getProperty('company')}"
            + "_#{getProperty('appname')}/q1";  // #tmp is only token being replaced
                                                // "hardcoded", not passed to Spring
    

    with

        String realPath = expParser.parseExpression(folderPattern, templateParserContext)
                .getValue(stdEvaluationContext, applicationContext.getEnvironment(), String.class);