Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
EL Context Framework Design
Contents
Motivation
In EL expressions, user and system-defined symbols can be used to store and retrieve values as well as point to methods that can be called. For example, the following expression references a property on some object:
#{object.property}
In JSP 2.1/Faces 1.2, the job of resolving 'object' and 'property' to values that can be used in evaluating the expression is delegated to an API defined in the Unified EL specification. The root of the API is an class called 'ELContext'. Each implementer of Unified EL can create and populate one or more ELContext object for use in evaluating expressions.
This document lays out the design of a 'design-time framework' for EL symbol resolution that allows tooling to support technologies that use Unified EL. The emphasis is on supporting JSF and JSP technologies. An effort is also made to remain as backward-compatible with previous tooling as possible without shackling new design.
Restatement of Requirements
There are three major user-stakeholders in this framework. The first is Developer (specialized as JSFDeveloper for JSF tooling). Developer needs to be able to support EL at design-time any place they may use it at runtime. This user also needs a way to install additional support for dependent behaviour for things like third-party libraries.
The second stakeholder is LibraryDeveloper. LibraryDeveloper classifies any users who wishes to have a library that uses Unified EL to be supported by the tooling. In JSP an example of LibraryDeveloper is anyone creating JSP tag libraries; in JSF a specialized example is the JSFComponentLibraryDeveloper. LibraryDeveloper needs to be able to contribute supplementary support for their library to default framework behaviour.
The third user-stakeholder is ToolsDeveloper. ToolsDeveloper uses the tooling to create specialized tooling that needs to support EL technologies. This user needs to be able to modify any default behaviour to suit specialization to the standard technologies being supporting by the tooling.
Within this context, framework design goals will emphasize the following requirements:
- The framework must provide a way for Developer to have EL expression resolution wherever it is useful and for LibraryDeveloper and ToolsDeveloper can easily provide it.
- The framework must be flexible enough to allow each web application (as defined at design time by a dynamic web project rooted in an IProject) to have entirely different symbol resolution in different application contexts. This supports the needs of ToolsDeveloper. For example, Faces 1.2 supports two different ELResolvers depending on where an expression is being resolved.
- The framework must allow third parties to modify the way symbols are resolved in each of the possible application contexts. This supports the needs of LibraryDeveloper.
- The framework must provide a simple but comprehensive way for Developer to manage these new behaviors on each web application within the constraints of providing correct design time behavior. This includes managing third-party contributions and the ability to enable or disable functionality in ways that match what could happen at runtime.
Why EL symbols are resolved at design-time
The actual value of an EL symbol is normally not computable at design-time because it is often dependent on runtime state, user input and so on. However, with many of the more widely used EL symbol objects like POJO beans and resource bundles, enough information can be predicted about runtime semantics to provide useful tooling support. In some cases like resource bundles, it is even possible to evaluate the actual value of a symbol and aid the user further.
Major users of design-time resolution information include:
- Validation: type information can be derived and checked against what is expected.
- Content-assist: an object's possible methods and properties can be determined and presented to the user as code assist tips.
- Refactoring: When underlying objects change, EL that was valid may become invalid. The user can be presented with automatic refactoring options when underlying dependencies change.
How EL symbols are resolved at design-time
In order to predict the runtime type and other semantics of an EL symbol, the design-time framework needs to parallel as closely as possible the way it is evaluated at runtime. In that respect, the EL Context Framework mirrors the runtime Unified EL API by creating "design-time versions" of the API. Class and interface naming generally follows the convention of taking the runtime API name and prepending "DT" to it.
The "root" of the API class hierarchy is the ELContext object. The ELContext contains resolver objects that can map EL symbols into meaningful runtime objects and values. The design time framework therefore defines a "DTELContext" object that contains similar design-time resolvers. The most important classes are listed below:
File:ELContext class diagram.png
All of the classes in this diagram except for DTELResolverDescriptor are direct mirrors of runtime equivilents. DTELResolverDescriptor provides meta-data about a DTELResolver to help ToolsDeveloper add configuration support and will be discussed later.
DTELResolver is used to resolve model objects, properties and method names as the following example:
#{bean.property}
In the first example, the symbol 'bean' would be passed to the DTELResolver to determine if it is a valid model object. If it is, the DTELResolver would then be asked if 'property' is a valid property of that object. The result of each request to DTELResolver is symbol information returned conforming to the following ISymbol API:
A 'symbol' is modelled broadly as simply a named object. The descendents correspond to the main types of named objects: model objects (i.e. managed beans, resource bundles), properties and methods. From there objects are further specialized for specific uses such as IBeanInstanceSymbol that is used to model instances of managed beans. The model is intended to further extended by LibraryDeveloper and ToolsDeveloper as needed.
FunctionMapper allows a JSP-style function to be resolved to an executable Method object at runtime. In previous versions of EL, this was generally done through the tag library function mechanism. Unified EL generalizes this so that each implementer can determine how such functions are resolved. For example, the expression:
#{ns:f(1)}
decomposes into a namespace, "ns" and a function name "f()". It the responsibility of the runtime FunctionMapper to map these two pieces of information to a callable method. Similarly, the design-time framework defines DTFunctionMapper to resolve design-time information for such a method. TODO: an open problem is still determine whether a the result of querying DTFunctionMapper should map to a new type of ISymbol, or whether it could map directly to something more tangible like a Method (Java introspection framework) or a JDT IMethod object.
VariableMapper is used at runtime to allow an entire EL expression to become a named, first-class object in the symbol namespace. At design-time, DTVariableMapper is used to store and retrieve the design-time equivalent. For example, consider this piece of JSP code:
<c:forEach var="book" items="\#\{BooksBean.books\}"> <h:inputText id="quantity" value="\#\{book.quantity\}" ... /> </c:forEach>In this case, the "value" attribute of the JSF tag, "h:inputText" uses the "book" symbol defined in the JSTL tag "c:forEach". The value of the "book" variable in this case is the expression
#{BooksBean.books[i]}where i is the element of the BookBean.books collection selected at each iteration of the loop. So DTVariableMapper must also support a way to communicate symbol information between different EL Contexts -- in this case, between JSF and non-JSF tag contexts.
TODO: do we use the symbols framework for variable mapper?
Supporting multiple EL Contexts
Each runtime implementer of EL can define one or more ELContext objects for use in different application contexts to help resolve expressions. Both JSP and JSF define their own ELContexts.
The framework supports this by defining two new extension points: model context specifiers and DTELContext contributors. Model context specifiers define a specific context-space in which a single DTELContext may wish to operate. DTELContext contributors are simply a way to contribute new DTELContext objects to the list available to Developer to use. Developer can configure which DTELContext contribution is used in which model context by using a new project property page:
One row in the dialog is constructed per registered Model context specifier. The combo box for each specifier is populated with each DTELContext contribution that indicates that it supports that model context. The current selection in each combo box indicates the DTELContext that will be used when EL expressions are resolved for that model context.
Note that there are three main states for the DTELContext combo box corresponding to the three possible options the EL framework has for resolution in each context:
1) a DTELContext is available, supported and selected for a model context specifier. The first three rows show this state. This is a normal, non-error case.
2) a DTELContext is available and supported, but Developer has indicated that no DTELContext should be selected for a model context. This is shown in row four of the dialog. The user may choose to do this if all DTELContexts seem to be behaving incorrectly or are not providing desired behaviour. This is an exceptional, non-error case. Note that a message is placed in the combo box to indicate the exceptional case.
3) a model context specifier has been registered, but no DTELContext's exist that support that model context. This is an exceptional, non-error case since it would be unusual to have model context without a DTELContext, but it is a supported behaviour because model contexts are decoupled from DTELContext's. Not that in this case, the combo box is disabled and a message is set to indicate an exceptional case to the user. TODO: Should we add a warning marker?
The following diagram illustrates the main entities involved in defining Model context specifiers and selecting related DTELContexts.
The diagram can be seen as having two columns of entities: the left column shows the main management classes and the right column shows the important classes that are managed. The DTELContextSelection encapsulates the matching of a model context specifier and a DTELContext. The DTELSelector provides an interface for clients to request a DTELContext in a particular model context. DTModelContextManager manages the collection of all registered model context selection extensions. DTModelContextSpecifier encapsulate a single model context specifier extension.
A DTModelContextSpecifier consists one or more description elements that define the specifics of the context. The IModelContext kind of specifier describes static context in a particular content model -- for example, a line and column number in a JSP document. The StaticContextDescriptor can be used to collect otherwise disparate information about a static context: for example, it may be used to define a context as being within one or more Content-Types or one or more DOM elements within a particular XML DOM tree. The DTModelContextSpecifier is intended to allow a contributor to generalize context in very specific application locations. For example, in JSF an important context for EL is within the attributes of JSF tags. To specify this, we must be able to say "only JSP content types" and "only within DOM attributes of elements that map to JSF tag libraries". TODO: further design work is required here.
A DTELContext is generally used by a symbol resolver during EL resolution. The following sequence diagram shows how a DTELContext is acquired:
In this case, an entity called ISymbolContextResolver (this is a JSF interface used to resolve symbols in a specific IModelContext), has a model context specification (modelContext) for which it wishes to acquire the DTELContext. It passes this request to the DTELContextSelector. DTELContextSelector checks if it has a DTELContextSelection matching this specifier. If it does (the normative case), it retrieves it and returns its DTELContext object. Note that the DTELContextSelection here is determined from what is configured in "Configure Active EL Contexts" dialog. TODO: is there a need for a 'default' selection in cases where the user does not configure it? Can this mechanism be set programmatically by ToolsDeveloper to override these defaults with their own?
Finally, to elucidate how the various entities inter-relate, below is a sequence diagram showing how the "Configure Active EL Contexts" dialog would be constructed:
Configurable DTELResolvers
One of the most common ways for a Developer to modify EL resolution behaviour at runtime is by modifying an ELResolver. As previously stated, the ELResolver anchors all the symbol resolution for model objects, properties and methods (excluding EL functions). Also LibraryDeveloper may want to introduce new symbols through their libraries and components. Since this modification is common and likely, and because it touches at least two of our major user-stakeholders, the framework includes direct support for making DTELResolvers flexible and configurable.
Below is the class hierarchy of all design-time ELResolvers that framework must implement to mirror the basic Unified EL API:
Note that DTCompositeELResolver contains a collection of DTELResolvers. This mechanism is used to allow more than one DTELResolver to be consulted with a single request (matching the runtime API behaviour). In the runtime CompositeELResolver there is an add() operation that allows clients to add addition resolvers at runtime. DTCompositeELResolver supports an analogous (not shown in diagram) operation). This drives the need for our first two extension points: composite resolver contribution id and composite resolver contribution.
The composite resolver contribution id allows any DTCompositeELResolver to define a unique id. Any contributor can then implement a composite resolver contribution extension that identifies a DTELResolver to be added to the DTCompositeELResolver. It is up to the implementer of each DTCompositeELResolver to decide if a contribution is added and where in its list (and therefore what precedence it has) it will be added.
The following class diagram shows the major entities in this extension:
Each DTCompositeELResolver may have zero or more CompositeELResolverContributionIds. This allows it to identify multiple types of contributions. Each CompositeELResolverContributionId collects all of the contributions made to it as CompositeELResolverContributions.
The second area where DTELResolvers can be usefully extended is where generalized runtime behaviour is known to add symbols into the symbol space of a particular ELResolver. The best example of this scenario is with the ScopeAttributeELResolver. This is implemented by both JSP and JSF to resolve symbols that are added to runtime request, session and application maps. This can be done programmatically at any time in code called by either technology, but there is a generalized case that is important to support: where a JSP tag, either JSP or JSF-specific, declares a model object symbol in an attribute. The JSTL tag c:forEach does this with it's 'var' attribute and the JSF f:loadBundle tag does something similar. For LibraryDeveloper, it is important to able to add support for their own tags.
A mechanism is needed by which a DTELResolver may accept contributed delegates that, rather than add new DTELResolver's, contribute possible symbols to the symbol space of the resolver. Two new extension points are defined structured in the same way as the above composite resolver contribution. These are context symbol factory id and context symbol factory. Any DTELResolver may define zero or more context symbol factory ids. Any number of contributors may then register a context symbol factory to that id which the DTELResolver may use to increase its knowledge of symbols that exist in a particular context.
Unlike the composite resolver contribution, a context symbol factory may require a specific model context with which it modifies what symbols it returns. This is necessary to support the scoping of some model objects. For example, in the case of this c:forEach code snippet:
<c:forEach var="book" items="\#\{BooksBean.books\}"> <h:inputText id="quantity" value="\#\{book.quantity\}" ... /> </c:forEach>
Here the model object declared by 'var="book"' is only valid inside the c:forEach tags. Therefore, a context symbol factory may use a model context to return the model object 'book' if the request is made to resolve the expression in the above 'value' attribute, but won't return it somewhere else in the JSP document.
To satisfy the needs that Developer is able to configure these third-party contributions directly, the following UI is provide as a project properties configuration dialog:
In the left-hand tree is listed every available DTELContext. The right-hand tree shows the current configuration for the DTELResolver belonging to the currently selected DTELContext in the left-hand tree. In this example, the DTELContext for JSF tag attributes is selected. The right-hand tree shows the chain of DTELResolvers belonging to this context as they are currently installed. Note that the "Scoped Attribute EL Resolver" has a child in the tree "JSF tag context symbol factory" that is currently selected. This is a context symbol factory contribution made to the scoped attribute DTELResolver. In the composite below the right-hand tree is illustrated the type of information that would be displayed for each such contribution. The most important one is the "Enabled" flag that would allow Developer to override each such contribution.
To support querying and modifying state information for resolver, each DTELResolver must implement a corresponding DTELResolverDescriptor as shown in above diagrams. This descriptor provides an interface through which a DTELResolver's enabled state can be set and its contribution information can be queried. It may also expose label and content provider interfaces useful in displaying its structure in tree form as shown in the dialog.
Appendix A: Summary of New Extension Points
There are six new extension points defined by this design specification:
Extension Name | Purpose |
---|---|
Model context specifier | Allows new model contexts to be created. A model context defines a part of a model (typically a content model for a document) that has need of a specific kind of ELContext for EL resolution. Each model context may have exactly 0 or 1 DTELContext associated with it any time, but more than DTELContext may be available that supports the model context. It is up to the user and tools developers to select the active one. |
DTELContext contributor | Allows any number of third-parties to contribute any number of DTELContexts. Each DTELContexts can support zero or more model context specifiers although only one can be active for each model context at one time. |
Composite resolver contribution id | Allows a composite resolver to define an identifier. Third-parties may use this identifier to add additional resolvers to the composite through the composite resolver contribution extension point. |
Composite resolver contribution | Uses a 'composite resolver contribution id' identifier to register a new DTELResolver with a DTELCompositeResolver. This is only possible if the DTELCompositeResolver has declared itself modifiable in this way be supporting the identifier in question. |
Context symbol factory id | Allows any DTELResolver to define an identifier. Third-parties may use this identifier to add symbol factory delegates to the resolver. |
Context symbol factory | Uses a context symbol factory id to register a symbol factory delegate with a supporting DTELResolver. The symbol factory may be context sensitive. It may be called (subject to possible veto by the DTELResolver owner and by user configuration) to add additional symbols to those known a priori by the DTELResolver. This extension exists primarily support situations such as those in JSP where a tag may dynamically add new symbols at runtime. |