Search code examples
javaelel-processor

How to define variable with namespace in EL processor?


In some XML files I saw something like this <tag attr="${sys:com.foo.bar}"/> where it is meant that ${sys:com.foo.bar} will be replaced to System.getProperty("com.foo.bar").

I want my EL processor to support such feature. However, I can't find how to do it.

ELProcessor elProcessor = new ELProcessor();
elProcessor. ???

The only method that has prefix is defineFunction, but as I understand this is not what I need:

this.elProcessor.defineFunction(prefix, function, method);
this.elProcessor.defineFunction(prefix, function, className, method);

Could anyone say how to make my EL processor to work with variables with namespaces?


Solution

  • You've undoubtedly seen this in Maven / Spring / Log4j related XML configuration files. Unfortunately, they don't use Jakarta EL to resolve them. They use under the covers Apache Commons Text StringSubstitutor for this. And, Jakarta EL doesn't support this syntax in first place. It would already die with a syntax error before even having the chance to hit any custom EL listener or EL resolver.

    With Jakarta EL, your best bet is to change the syntax to ${sys['com.foo.bar']} and add a custom ELResolver which recognizes ${sys} as a special kind of base and then resolves the rest via System#getProperty().

    Here's a kickoff working example of such EL resolver:

    public class SystemPropertyELResolver extends ELResolver {
    
        @Override
        public Class<?> getCommonPropertyType(ELContext context, Object base) {
            return (base == this) ? String.class : null;
        }
    
        @Override
        public Class<?> getType(ELContext context, Object base, Object property) {
            if (base == null && "sys".equals(property)) {
                context.setPropertyResolved(true);
                return getClass();
            }
            else if (base == this && property != null) {
                context.setPropertyResolved(true);
                return getCommonPropertyType(context, base);
            }
    
            return null;
        }
    
        @Override
        public Object getValue(ELContext context, Object base, Object property) {
            if (base == null && "sys".equals(property)) {
                context.setPropertyResolved(true);
                return this;
            }
            else if (base == this && property != null) {
                context.setPropertyResolved(true);
                return System.getProperty(property.toString());
            }
    
            return null;
        }
    
        @Override
        public void setValue(ELContext context, Object base, Object property, Object val) {
            // NOOP.
        }
    
        @Override
        public boolean isReadOnly(ELContext context, Object base, Object property) {
            return true;
        }
    
        @Override
        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
            return null;
        }
    
    }
    

    In order to get it to run, register it as follows:

    elProcessor.getELManager().addELResolver(new SystemPropertyELResolver());
    

    However, in the specific case of System#getProperty(), there's another, yet simpler, way to "define a variable with namespace". As System#getProperties() already returns an instance of java.util.Map, the built-in jakarta.el.MapELResolver could be used to take care of it while using the same syntax of ${sys['com.foo.bar']}.

    elProcessor.defineBean("sys", System.getProperties());
    

    This only doesn't offer you the opportunity to further customize the EL resolving.