Technical Note: Validation Client Context Bindings and Traversal Strategies

Version: 0.4 Date: 2007/10/31

Contents


Introduction

[back to top]

This document describes the EMF validation framework's facility for clients to bind constraints to a "client context" that they define. This effictively allows applications to choose the constraints from the library of available constraints that they want to apply to the elements of their models.

Closely related to client contexts is the comcept of traversal strategy, which is a pluggable algorithm for traversing models. EMF's basic content-tree traversal is not suitable for all metamodels: sometimes non-containment relationships must be traversed to find more model content or branches of the content tree can be skipped entirely, and some clients may even provide end-user customization of the traversal.

References

[back to top]

Traceability

[back to top]

Requirements

[back to top]

This feature must satisfy the following requirements:

Constraints

[back to top]

This feature is subject to the following design constraints:

This feature has the following limitations, which may be considered for future enhancements:


Basic Principles

[back to top]

The concept of a "context" is modeled loosely after the Eclipse IContext API. This API is not used because it is defined in the org.eclipse.ui.contexts plug-in and because validation contexts do not need to be either dynamic or hierarchical.

Applications specify bindings in XML between a context that they declare and constraint IDs defined in the constraint library. In order to determine the contexts that are associated with a particular model element (as there may be multiple), a context declares an enablement expression (using the Eclipse standard expression language) that is satisfied for objects belonging to the context. Where this language is not sufficient, a selector class may be declared instead that "recognizes" objects that belong to the context. An enablement expression effictively implements an XML selector.

Compatibility

Backward compatibility is provided by the notion of a "default client context." A context defined by any plug-in may declare that it is a default context, which will be implicitly bound to those constraints that do not have any explicit context bindings in any client. Obviously, all constraints defined by clients of prior releases of the framework fall into this category.

Robustness

The validation system protects itself against problems in the initialization and execution of client contexts and their selectors. Problems in the initialization of a client context (missing selector, invalid XML expression, invalid selector class) are logged with appropriate diagnostic information and result in the context not being defined.

When a context is successfully initialized, any CoreException thrown by the expressions API in evaluating an enablement expression or any run-time exception thrown by a custom selector implementation is logged and results in the context being removed from the system. Thus, all dependent constraint bindings cease to exist. It is anticipated that, in typical models, any such exceptions will occur repeatedly on similar elements or editing gestures and, therefore, the framework would expect to spend much time processing exceptions if it did not discard the context. Besides, these exceptions are usually indicative of programming errors.

Performance

In the interest of performance in batch validation of large models, it is desirable to infer as much as possible the contexts of an element from the elements previously traversed. Thus, the traversal strategy determines when the client context needs to be computed for an element, depending on the points in a traversal where it may likely enter another client domain.

Note: Live validation does not consider traversal of the model structure. It will compute the contexts applicable to each validated element, individually.

There are two extreme performance profiles to consider: at one extreme, a traversal assumes that client context is determined exclusively by a traversal root (i.e., one of the elements selected by the user in the UI). At the other extreme, the traversal strategy computes the contexts on every element that it encounters.

Examples

[back to top]

Defining a Client Context

Client contexts are declared on the constraintBindings extension point:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <clientContext id="org.eclipse.emf.validation.example.MyClient">
      <enablement>
         <and>
            <instanceof value="org.eclipse.emf.ecore.EModelElement"/>
            <test
               property="org.foo.bar.someProperty"
               value="someValue"/>
         </and>
      </enablement>
   </clientContext>
</extension>

When the expression language is not effective in specifying the matching condition, a custom selector may be provided:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <clientContext id="org.eclipse.emf.validation.example.MyClient">
      <selector class="org.eclipse.emf.validation.example.MyClientSelector"/>
   </clientContext>
</extension>

with a particular selector implementation perhaps looking like:

public class MyClientSelector implements IClientSelector {
    public boolean selects(Object object) {
        boolean result = false;
        
        if (object instanceof EObject) {
            // it should be an EObject, but we'll be defensive
            EObject eObject = (EObject) object;
            
            Resource res = eObject.eResource();
            
            if (res != null) {
                result = isEcoreContentType(res);
            }
        }
        
        return result;
    }
    
    private boolean isEcoreContentType(Resource res) {
        // ... determine whether 'res' is an Ecore
        //   model and doesn't just contain EModelElements
    }
}

Binding Constraints to a Client Context

Client contexts can be bound to constraints, individually, or to constraint categories (to bind all of the constraints in the category). The latter option has the advantage of allowing new constraint contributions in a category to automatically be bound to the appropriate client context, even if the constraint is defined in a plug-in that is unaware of that context or its binding to the category. Category bindings are inherited by sub-categories from their ancestors.

Example of a binding for an hypothetical "EClass::eAllSuperClasses are acyclic" constraint in Ecore:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <binding
      context="org.eclipse.emf.validation.example.MyClient"
      constraint="org.eclipse.emf.validation.example.ecore.eclasses.genCycle"/>
</extension>

An alternative form that is slightly less cumbersome when binding multiple constraints to a context:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <binding context="org.eclipse.emf.validation.example.MyClient">
      <constraint ref="org.eclipse.emf.validation.example.ecore.epackages.distinctEClassifiers"/>
      <constraint ref="org.eclipse.emf.validation.example.ecore.eclasses.genCycle"/>
   </binding>
</extension>

Example of a binding for all of the constraints defined in an hypothetical "Ecore" constraint category:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <binding
      context="org.eclipse.emf.validation.example.MyClient"
      category="org.eclipse.emf/ecore"/>
</extension>

The alternative form of category bindings looks like:

<extension point="org.eclipse.emf.validation.constraintBindings">
   <binding context="org.eclipse.emf.validation.example.MyClient">
      <category ref="org.eclipse.emf/ecore"/>
      <category ref="org.eclipse.emf/ecore2xml"/>
   </binding>
</extension>

Note that nested <constraint> and <category> elements can be freely intermixed.

Defining a Traversal Strategy Extension

Implementers of EMF-based metamodels are encouraged to provide a default traversal strategy for their metamodel, as they control its structure. Considerations for traversal include

Default traversal strategies are contributed on the traversal extension point. For example, a traversal extension for the Ecore metamodel might look like:

<extension point="org.eclipse.emf.validation.traversal">
   <traversalStrategy
         namespaceUri="http://www.eclipse.org/emf/2002/Ecore"
         class="org.eclipse.emf.validation.example.EcoreTraversalStrategy">
   </traversalStrategy>
</extension>

A slightly more complex example might provide different strategies for EPackages as for other elements. The validation framework determines the appropriate traversal strategy from the root container of a traversal root, considering that root containers define a "kind" of model.

<extension point="org.eclipse.emf.validation.traversal">
   <!-- The default strategy for non-packages. -->
   <traversalStrategy
         namespaceUri="http://www.eclipse.org/emf/2002/Ecore"
         class="org.eclipse.emf.validation.example.DefaultEcoreTraversalStrategy">
   </traversalStrategy>

   <!-- The strategy for EPackages. -->
   <traversalStrategy
         namespaceUri="http://www.eclipse.org/emf/2002/Ecore"
         class="org.eclipse.emf.validation.example.EPackageTraversalStrategy">
      <eclass name="ecore.EPackage"/>
   </traversalStrategy>
</extension>

A simple traversal strategy for Ecore might skip over all EAnnotations, knowing that all of the EClasses in this metamodel ultimately extend the Ecore EModelElement metaclass. Note that this simple example doesn't handle cases where one traversal root is in the sub-tree of another, nor does it improve upon the inherited isClientContextChanged() implementation.

public class DefaultEcoreTraversalStrategy
        extends AbstractTraversalStrategy {
    
    protected Iterator createIterator(
            final Collection traversalRoots) {
        
        return new Iterator() {
            private TreeIterator delegate =
                EcoreUtil.getAllContents(traversalRoots);
            private Object next;
            
            public boolean hasNext() {
                while (next == null && delegate.hasNext()) {
                    next = delegate.next();
                    if (next instanceof EAnnotation) {
                        next = null;
                        delegate.prune();
                    }
                }

                return next != null;
            }

            public Object next() {
                if (!hasNext()) {
                    return new NoSuchElementException();
                }

                Object result = next;
                next = null;
                return result;
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    protected int countElements(Collection traversalRoots) {
        int result = 0;

        Iterator iter = createIterator(traversalRoots);

        while (iter.hasNext()) {
            result++;
            iter.next();
        }

        return result;
    }
}

Using a Custom Traversal Strategy

If the default traversal strategy registered on an extension point such as in the previous example isn't sufficient for a client application's needs, then it may override that default strategy when it performs a validation.

IProgressMonitor monitor = // ... get a progress monitor
Collection selectedElements = // ... elements to validate

IBatchValidator validator = (IBatchValidator)
    ModelValidationService.getInstance().newValidator(
        EvaluationMode.BATCH);

validator.setTraversalStrategy(new MyTraversalStrategy());

IStatus status = validator.validate(selectedElements, monitor);

// ... do something with the status

Design/Code/Interaction Models

[back to top]

The main part of the client context API is depicted in the figure below.

The ClientContextManager is responsible for loading the client contexts and bindings from the constraintBindings extension point, and for filtering the constraints applicable to an object according to its client context.

The existing IModelConstraint interface is not changed by this feature.

The sequence diagram below shows how the framework uses the ClientContextManager to filter the constraints that it applies to an EObject. Note that, for performance considerations, the flow may not be exactly as indicated. This sequence is meant to show the interaction in concept only.

The steps are as follows:

  1. The validation framework asks the context manager for all client contexts that apply to a specific EObject (as a traversal root). The context manager iterates over the available contexts:
    1. The context manager requests the selector of a context.
    2. The context returns the selector.
    3. The context manager queries the selector whether it matches the specified EObject.
    4. The selector returns true or false as appropriate.
  2. The context manager returns all of the contexts whose selectors matched the EObject.
  3. The validation framework iterates the contexts returned by the context manager:
    1. The framework requests a context's bindings from amongst the constraints that target the EObject's metaclass. The context manager iterates the constraints:
      1. For each constraint, the context manager queries the context whether it includes the constraint.
      2. The context returns true or false as appropriate.
    2. The context manager returns the set of constraints that the context includes.
    The validation framework then goes on to apply the resulting constraints to the EObject.

The relationships between the types in traversal strategy API is depected in the figure below.

API Elaboration

[back to top]

The context binding and traversal strategy API and implementation are provided by the org.eclipse.emf.validation plug-in.

Constraint Bindings Extension Point

The constraintBindings extension point is a public API extension point in the org.eclipse.emf.validation namespace.

Traversal Strategies Extension Point

The traversal extension point is a public API extension point in the org.eclipse.emf.validation namespace.

IClientSelector

public abstract boolean selects(Object object)
This method is implemented by custom client context selectors to determine whether the context includes the specified object, returning true if such is the case, otherwise false. Anything about the input element can be used to make this determination, subject to only a few restrictions: The input to a custom selector implementation will, in general, be an EObject, though it is prudent to check that such is the case. The signature of this method is Object primarily to assist the enablement expression support.

IClientContext

This class is not intended to be used by clients of the validation framework; it does not constitute a part of the API.

ClientContextManager

This class is not intended to be used by clients of the validation framework; it does not constitute a part of the API.

ITraversalStrategy

public abstract void startTraversal(Collection traversalRoots, IProgressMonitor monitor)
Provides the collection of EObjects to be traversed and a progress monitor to update as traversal progresses. The implementer is expected to configure the progress monitor with the amount of work to be done (according to the number of objects to be traversed).
Note that traversal strategy instances are re-used, so that this method may be invoked any number of times.
public abstract boolean hasNext()
Queries whether there are any elements remaining to be traversed. If this method returns true, then next() must return a valid EObject.
public abstract EObject next()
Returns the next model element in the traversal sequence. If hasNext() is false, then the implementer should throw a java.lang.NoSuchElementException.
Traversal is acyclic: the implementer must never return an element that has already been traversed since the last invocation of startTraversal().
public abstract boolean isClientContextChanged()
Queries whether the next() element in the traversal can possibly be in a different client context than the previous. This will usually occur when the traversal follows a non-containment relationship or bridges to another metamodel. When this method returns true, the framework will re-compute the current client context from the next element. Thus it is safe to always return true, but considerably less efficient.
public abstract void elementValidated(EObject element, IStatus status)
Called by the framework to inform the traversal of the result of validation of the last element returned from the next() method. This can be used by adaptive traversals to prune branches of the sequence or to otherwise alter their direction according to the status, and is recommended as an opportunity to update the progress monitor.

ITraversalStrategy.Flat

A simple implementation of the traversal strategy which simply iterates the traversal roots and does not descend into their containment trees or follow any other kinds of reference.

ITraversalStrategy.Recursive

A slightly less simple strategy than the Flat, which has the following characteristics:

AbstractTraversalStrategy

A convenient superclass for custom traversal strategy implementations. In particular, it takes care of managing the progress monitor. Subclasses provide an Iterator to make elements available. Note that this class's implementation of the isClientContextChanged() method is pessimistic: it always returns true. Subclasses should consider overriding this to take advantage of the particular metamodel structure.

protected abstract int countElements(Collection traversalRoots)
This method is invoked by the implementation of the startTraversal() method to determine the total amount of work for the progress monitor (one unit of work per element). A simple implementation might just count the number of elements returned by the iterator created by createIterator(), but there may be smarter ways. A subclass may even provide a different mapping of work units to elements, in which case the inherited implementation of elementValidated() should also be overridden.
protected abstract Iterator createIterator(Collection traversalRoots)
Invoked by the abstract class to access the elements of the traversal.

IBatchValidator

Most of the methods of the IBatchValidator interface do not relate particularly to the traversal strategy API. The exceptions are described in this section.

public abstract void setTraversalStrategy(ITraversalStrategy strategy)
Configures the batch validator with a custom traversal strategy. This is useful for clients that have specific traversal requirements that are not satisfied by the default strategy for the metamodel (which is contributed on the extension point). For example, an application implementing a client context for constraint bindings may want to tailor the traversal strategy to match, for efficiency. The possibilities for customization include pruning of uninteresting sub-trees and determination of when the client context may change.
public abstract ITraversalStrategy getTraversalStrategy()
Retrieves the current traversal strategy used by the validator. Initially, this will be the default strategy (see below), but it may be overridden by the client via the setTraversalStrategy() method.
public abstract ITraversalStrategy getDefaultTraversalStrategy()
Obtains a default traversal strategy which delegates to implementations of the extension point, if available for the particular metamodel. Otherwise, the default-default strategy for a metamodel is the ITraversalStrategy.Recursive.
The default traversal strategy can be assigned to the batch validator at any time to undo the client's override.

Copyright 2005, 2007 by IBM. Made available under EPL v1.0