el-next@uel.java.net

Re: Proposal: ELProcessor

From: Mike Brock <cbrock_at_redhat.com>
Date: Fri, 9 Apr 2010 12:37:22 -0400

I have to say that I'm not a big fan of the current concept of a context. Of course, I'm really biased.

As far as definitions go, what this spec calls a "ParseContext", MVEL calls a "VariableResolverFactory". MVEL's ParserContext contains literally only compiler configuration information, and optionally type ingress/egress information for statically typed expressions. MVEL's parser does not need to know about variables at parse/compile time really -- at least for dynamic expressions. And there's really no rhyme or reason technically to require it, unless you're requiring ingress type checking.

In MVEL you can literally do this:

Integer result = MVEL.eval("10 * 2", Integer.class);

 Or you can do this:

Map<String, Object> varMap = new HashMap<String, Object>();
varMap.put("a", 10);
varMap.put("b", 2);
Integer result = MVEL.eval("a * b", varMap);

Or you can do this:

VariableResolverFactory factory = new MapVariableResolverFactory(new HashMap());
factory.createVariable("a", 10);
factory.createVariable("b", 2);
Integer result = MVEL.eval("a * b", varMap);

... and of course, the second example is really just API sugar that equates to the last example, as the resolver factory API is really what MVEL's runtime uses under the hood.

MVEL's architecture intentionally goes out of it's way to separate the concepts of runtime state, and the parser/compiler. Mainly for performance reasons, which I can get into later. But it's always made more logical sense to me. It also has the side-effect of being able to push data in, have the script manipulate that data, and then be able to extract it back out easily.

Where MVEL's ParserContext comes into play is largely when you get into the world of static typing -- which MVEL supports. So you'd really create a context for the parser that is configured to enforce type safety, and also hold ingress type vectors.

ie.

String expression = "map.size()";

ParserContext ctx = ParserContext.create()
                .stronglyTyped()
                .withInput("map", Map.class);

Serializable s = MVEL.compileExpression(expression, ctx);

Map<String, Object> vars = new HashMap<String, Object>();
vars.put("map", new HashMap());

assert 0 == MVEL.executeExpression(s, vars);

To me, this logical separation between configuration of the parser, and program state makes the most sense. It allows the API to contain much more sensible defaults for ad-hoc usage, and it encourages better design of the underlying architecture IMO.

And if my users are to be believed, it's much more easier for them to understand and work with than the approaches taken by other architectures. Ultimately, this is is a specification, so I realize it will have multiple implementations, and therefore needs some sort of factory approach to obtain the underlying implementation and work with it -- but I still think there's more room for moving the API towards sensible defaults.

Mike.

On 2010-03-18, at 8:41 PM, Kin-man Chung wrote:

>
>
> On 03/17/10 18:40, Christoph Beck wrote:
>> Kin-man Chung wrote:
>>>
>>>
>>> On 03/15/10 15:41, Christoph Beck wrote:
>>>> Kin-man Chung wrote:
>>>>> From the discussions we;ve had so far, it has been clear that we need a
>>>>> simpler API for getting an ExpressionFactory and ELContext, outside of
>>>>> JSP or
>>>>> JSF. We'll need an object to setup a defualt for ELContext
>>>>> ELResolvers, etc. I'll call it ELProcessor, because it also can be
>>>>> used to simplify EL parsing and evaluations.
>>>>>
>>>>> Here's my tentative proposal. I'm trying to keep it simple and use
>>>>> a design that seems to fit in the current EL. Therefore there are no
>>>>> CDI extensions, or listeners. :-)
>>>>>
>>>>> Use example:
>>>>>
>>>>> ELProcessor elp = new ELProcessor();
>>>>> boolean t = elp.getValue("${'2 > 1'}", Boolean.class);
>>>>>
>>>>> An alternative is to make ELProcessor an interface instead of a
>>>>> concrete class, and use a ELFactory to create an instance of
>>>>> ELProcessor, as suggested by Pete in one of his mails.
>>>>>
>>>> So far I'm with you. However, I would try to avoid changing the existing
>>>> ELFactory API. The processor stuff should be a new layer on top of the
>>>> existing API without cyclic dependencies between the two layers.
>>>>
>>> But I'm not talking about ExpressionFactory, which I agree that we
>>> should not change for this purpose.
>>>
>> OK, I think I missed something... what is ELFactory?
>
> It's just a place to get a default implementation of ELProcessor, if
> ELProcessor is an interface. I'm inclined to just make ELProcessor
> an utility class, to keep things simple.
>
>>
>>>>> interface ELProcessor {
>>>>>
>>>>> ExpressionFactory getExpressionFactory();
>>>>> void setExpressionFactory(ExpressionFactory ef); // Note 2
>>>>>
>>>>> ELParseContext getELParseContext();
>>>>> void setELParseContext(ELParseContext elpc);
>>>>>
>>>>> ELEvaluationContext getELEvaluationContext();
>>>>> void setELEvaluationContext(ELEvaluationContext elec);
>>>>>
>>>>> ELResolver getELResolver();
>>>>> void setELResolver(ELResolver elr);
>>>>>
>>>>> // May also need getter/setters for function mapper and variables
>>>>>
>>>>> // Add an user defined ELResolver
>>>>> void addELResolver(ELResolver elr);
>>>>>
>>>>> // See note 4 below for BeanLookup
>>>>> void setBeanLookup(BeanLookup blu);
>>>>>
>>>>> ValueExpression createValueExpression(String expression,
>>>>> Class expectedType);
>>>>> Object getValue(String expression);
>>>>> Object getValue(String expression,
>>>>> expectedReturnType);
>>>>> MethodExpression createMethodExpression(String expression,
>>>>> Class expectedReturnType,
>>>>> Class[] extectedParamTypes);
>>>>> Object invoke(String expression, Object[] params, Class returnType);
>>>>> }
>>>>>
>>>> It may be a good idea to identifiy classes from the API that are pretty
>>>> "useless" to the end user. The ELProcessor should hide these classes
>>>> rather than expose them. More or less "useless" classes are:
>>>> - FunctionMapper (the user doesn't want to resolve functions; she wants
>>>> to add methods)
>>>> - VariableMapper (the user doesn't want to resolve variables; she might
>>>> want to add variables, but maybe it's better to have a setVariable(...)
>>>> method for this)
>>>> - ELContext (the user doesn't want to resolve properties, etc)
>>>> - ExpressionFactory (the user evaluates expressions through the
>>>> ELProcessor API and the ExpressionFactory requires an ELContext at hand)
>>>> - ValueExpression, MethodExpression (same as above)
>>>> So, I would suggest that these classes are candidates to _not_ appear in
>>>> the ELProcessor interface.
>>>>
>>> I agree with you in spirit about information hiding, but the ultimate
>>> question is really this: what is ELProcess? Is it just for
>>> casual users who would only want to evalute EL expressions, but nothing
>>> else, or is it something that allows for full EL customization? You
>>> seems to think it's the former, while I the later.
>>>
>> I was thinking of a simplified API which allows intuitive use of the EL,
>> supporting functions, variables and custom resolvers.
>>
>>> For instance, if we take away FunctionMapper, then EL functions cannot
>>> be used in the expression. I want to allow the user to be able to
>>> supply her own FunctionMapper, so that EL functions can be used.
>>>
>> ELProcessorBuilder.addFunction(String name, Method method);
>
> OK. We should continue our discussion in the other thread, about a more
> direct way of calling static functions.
>
>>
>>> Also, we should allow for deferred EL expressions, in that EL parsing
>>> and evaluation do not happen at the same time. Would you disallow this
>>> also?
>>>
>> OK, I omitted deferred evaluation to be able to hide the "useless"
>> classes and to reduce complexity. I think most users are "casual" users
>> and do not need it. But if I think of this again, there should be a way
>> to do this. I would let the user fall back to the core API somehow, e.g.
>>
>> ELProcessorBuilder.createValueExpression(String expression, Class<T> type);
>>
> You meant ELPRocessor, not ELProcessorBuilder?
>
>>>
>>>>> Notes:
>>>>>
>>>>> 1. The life time of ELProcessor instances should be controlled by
>>>>> the application, not EL. The application can use a single instance
>>>>> of ELProcessor for the entire application, or use an instance of
>>>>> shorter life. For instance, JSP and JSF applications can use one
>>>>> for application, and one in each of the servlet listeners.
>>>>>
>>>> Yes.
>>>>
>>>>> 2. I am not sure we need or even should allow setter for
>>>>> ExpressionFactory.
>>>>
>>>> I'm not sure we need a getter.
>>>>>
>>>>> 3. Objects in ELProcessor has the following defaults:
>>>>>
>>>>> a. ExpressionFactory: singleton from ExpressionFactory.newInstance().
>>>>> b. ELParseContext: with null function mapper and variable mapper.
>>>>> c. ELEvalutaionContext: with the default ELResolver, below
>>>>> d. ELResolver: a composite ELResolver, consists of:
>>>>> MapELResolver,
>>>>> ResourceBundleELResolver,
>>>>> ListELResolver,
>>>>> ArrayELResolver,
>>>>> BeanELResolver,
>>>>> BeanNameELResolver (proposed, see below)
>>>>>
>>>>> 4. Expressions such as #{foo.bar} will need a way to resolve "foo". I
>>>>> think
>>>>> bean discovery is the job of the bean containers, and not EL.
>>>>> Therefore I
>>>>> propose the following:
>>>>>
>>>>> interface BeanLookup {
>>>>> Object getBean(String name);
>>>>> }
>>>>>
>>>> We already have ELResolver and ELResolver.getValue(context, null,
>>>> property) does exactly this, so why invent a new thing? Also, the name
>>>
>>> Writing your own ELResolver is definitely non-trivial, not that it is
>>> technically hard, but that it requires a lot of typing and boiler-plate
>>> filling. This is one of the things people have been complaining
>>> about. BeanLoopup and BeanNameELResolver are proposed to solve this
>>> problem.
>>
>> But this only addresses top-level identifiers, a special case. Maybe we
>> can find something more general. Btw, I see some problems with BeanLookup:
>>
> Yes. But top-level identifiers are the import one, and is what most
> users care, and it's also the ones that don't have a default ELResolver
> to resolve them.
>
>> 1. side effects:
>> ...
>> processor.setBeanLookup(BeanLookup blu);
>> processor.setELResolver(ELResolver elr);
>> ...
>> Is my BeanLookup still there or kicked away?
>>
>> 2. how can I have multiple root resolvers?
>>
>
> I think setting a new BeanLookup can be implemented as
>
> processor.addELResolver(new BeanNameELResolver(blu));
>
> so 1. your BeanLookup will be gone,
> 2. by calling setBeanLookup again.
>
> Maybe we should rename it to addBeanLookup?
>
>>>
>>>> BeanLookup could be misleading as this would be used to resolve any
>>>> top-level identifiers. Not sure if it's worth having this.
>>>>
>>> BeanNameLoopup? getTopLevelBeans? Arg, I'm no good with names! :-)
>>
>> IdentifierResolver, RootPropertyResolver, TopLevelResolver, ...
>> (anything without "Bean" in it)
>
> I like IdentifierResolver. JSP used to have a VariableResolver with
> same functionality, but variable is already used in EL for something
> else.
>
>>>
>>>>> The method ELProcessor.setBeanLookup() let the application or container
>>>>> specify how to lookup a bean with the registered name.
>>>>>
>>>>> As an example, in the servlet listener, EL can be used to evaluate
>>>>> expressions involving an request scope object "foo":
>>>>>
>>>>> ELProcessor elp;
>>>>> elp.setBeanLookup(new BeanLookup {
>>>>> Object getBean(String name) {
>>>>> return request.getAttribute(name);
>>>>> }
>>>>> });
>>>>> elp.getValue("#{foo.bar}");
>>>>>
>>>>> BeanNameELResolver is a (proposed) ELResolver that uses the BeanLookup
>>>>> instance to resolve beans with names.
>>>>
>>>> OK, currently I have no clear idea of how to make resolving
>>>> identifiers/properties easier. This definitely needs further discussion.
>>>>>
>>> OK.
>>>
>>>>> 5. Of course we can also have different ELProcessors with slightly
>>>>> different
>>>>> defaults. The question is whether we need to specify them in the
>>>>> EL api, or just left as an implementation for the bean container.
>>>>> Ideally, the container should generate the appropiate ELProcessor
>>>>> depending
>>>>> on life cycle events of the applications, but that'll require tight
>>>>> coupling between EL and the container.
>>>>>
>>>>>
>>>>> Comments?
>>>>>
>>>> Here's what I could think of:
>>>>
>>>> interface ELProcessorBuilder {
>>>> void addVariable(String name, String expression);
>>>> void addVariable(String name, Object value);
>>>> void addFunction(String name, Method method);
>>>
>>> How are these for? Let me guess. Maybe I can do
>>>
>>> elProcessorBuilder.addVariable("x", "#{myBean}");
>>> elProcessor.getValue("#{x.foo}"); // returns myBean.foo ?
>>
>> Yes.
>>>
>>> but how do I use the other two methods?
>>>
>> Sorry. The ELProcessorBuilder configures an EL parser with variables and
>> functions. It provides an ELParseContext to the ELProcessor it creates.
>>
>> - ELProcessorBuilder.addVariable(String name, Object value) adds a
>> constant (delegates to ExpressionFactory.createValueExpression(Object
>> instance, Class<?> expectedType))
>> - ELProcessorBuilder.addFunction(String name, Method method) adds a
>> static method as a function
>>
> OK.
>
>>> Also, why are they not also in ELProcessor?
>>>
>> Once it has created an ELProcessor from it, subsequent changes to the
>> ELProcessorBuilder (e.g. adding funtions) do not affect the ELProcessor.
>> Btw, should I have called the ELProcessorBuilder ELFactory?
>>
> But your are assuming that the user wouldn't or shouldn't add variables
> or function mapper in between EL parsings/evaluations. I think setting
> variables to EL expression happens all the time. Also in JSP, a
> function mapper is created just before an expression is parsed, so that
> should be allowed also.
>
> I don't really see the need to have two classes (ELProcessBuild and
> ELPRocess) here.
>
>>>> ELProcessor buildProcessor();
>>>> }
>>>
>>> Now who builds ELProcessorBuilder?
>>>
>> I think this is a detail. It could also be a (utility) class.
>
> If there is only one implementation of it, it should just be a class.
>
>>
>>>>
>>>> interface ELProcessor {
>>>> void setPropertyResolver(PropertyResolver resolver);
>>> What is this?
>>
>> What do you mean? This method is used to set the
>> javax.el.PropertyResolver (often an instance of CompositeELResolver).
>
> But there is no javax.el.PropertyResolver! JSF has it though!
> How is this different from setELResolver()?
>
>>>
>>>> void setTypeConverter(TypeConverter converter);
>>>>
>>>> Object getValue(String expression, Class<?> type);
>>>> void setValue(String expression, Object value);
>>>
>>> Yes, I think this can be useful.
>>>
>>>> Object invoke(String expression, Object[] params, Class<?> returnType);
>>>> }
>>>>
>>>> class DefaultProcessorBuilder implements ELProcessorBuilder {
>>>> // convenience implementation
>>>> }
>>>>
>>> This discussion is useful, but we'll need to get into more details. It
>>> helps to include some examples for using new proposed features, to make
>>> things clearer.
>>>
>>> -Kin-man
>>
>> Revised proposal...
>>
>> /*
>> * Build parse context with variables and functions. No need to implement
>> FunctionMapper and VariableResolver.
>> * I'm proposing this as an interface, but it could also be an abstract
>> class or even a utility class. At least there should be a redy-to use
>> default implementation.
>> */
>> interface ParseContextBuilder {
>> void addVariable(String name, String expression); // add a value
>> expression as a variable, e.g. processor.addVariable("zero", "${0}");
>> void addVariable(String name, Object value); // add a constant as a
>> variable, e.g. processor.addVariable("zero", 0);
>> void addFunction(String name, Method method); // add static public
>> method as a function, e.g. processor.addFunction("math:sqrt",
>> Math.class.getMethod("sqrt", double.class));
>>
>> ELParseContext buildParseContext(); // create a parse context configured
>> with variables and functions from this builder.
>> }
>>
>> It would be great to have something similar for an ELEvaluationContext
>> (resolvers and converter).
>>
>> /*
>> * Build evaluation context with resolvers and converter.
>> * If no converter is set, a default implementation is used
>> * If no resolver is set, a default is used
>> * I'm proposing this as an interface, but it could also be an abstract
>> class or even a utility class. At least there should be a redy-to use
>> default implementation.
>> */
>> interface EvaluationContextBuilder {
>> void setPropertyResolver(PropertyResolver resolver); // set the
>> resolver; TODO: replace by methods to add simplified root/property
>> resolvers; needs discussion
>> void setTypeConverter(TypeConverter converter); // set the type
>> converter used during evaluation
>>
>> ELEvaluationContext buildEvaluationContext(); // create evaluation
>> context configured with property resolver and type converter from this
>> builder
>> }
>>
>> This does not clash with the ELProcessor but takes some responsibility
>> from it, i.e getELResolver(), setELResolver(ELResolver elr),
>> addELResolver(ELResolver elr), setBeanLookup(BeanLookup blu), as well as
>> methods to set/get FunctionMapper and VariableMapper could be dropped.
>>
> My main objection is that there are too many classes that the user has
> to use, and that variables, functions, ELResolvers cannot be added after
> ELContext is built. Let's discuss this more.
>
> Thanks
>
> -Kin-man
>
>> -- Christoph
>>>>
>>>>> ---------------------------------------------------------------------
>>>>> To unsubscribe, e-mail: el-next-unsubscribe_at_uel.dev.java.net
>>>>> For additional commands, e-mail: el-next-help_at_uel.dev.java.net
>>>>>
>>>>
>>>>
>>>> ---------------------------------------------------------------------
>>>> To unsubscribe, e-mail: el-next-unsubscribe_at_uel.dev.java.net
>>>> For additional commands, e-mail: el-next-help_at_uel.dev.java.net
>>>>
>>>
>>> ---------------------------------------------------------------------
>>> To unsubscribe, e-mail: el-next-unsubscribe_at_uel.dev.java.net
>>> For additional commands, e-mail: el-next-help_at_uel.dev.java.net
>>>
>>
>>
>> ---------------------------------------------------------------------
>> To unsubscribe, e-mail: el-next-unsubscribe_at_uel.dev.java.net
>> For additional commands, e-mail: el-next-help_at_uel.dev.java.net
>>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: el-next-unsubscribe_at_uel.dev.java.net
> For additional commands, e-mail: el-next-help_at_uel.dev.java.net
>