Tutorial: Working with OCL

Contents

Overview

The project includes a parser/interpreter for the Object Constraint Language (OCL) version 2.0 for EMF. Using this parser, you can evaluate OCL expressions on elements in any EMF metamodel. The following features are supported in the current version:

The following features are provided in addition to the OCL specification:

This tutorial will illustrate the various functions that the OCL parser can perform.

[back to top]

References

This tutorial assumes that the reader is familiar with the Eclipse extension point architecture. There is an abundance of on-line help in Eclipse for those unfamiliar with extension points.

To see the complete source code for the examples shown in this tutorial, install the OCL Interpreter Example plug-in into your workspace.

Other references:

[back to top]

Validating OCL Expressions

The first responsibility of the OCL interpreter is to parse OCL expressions. This capability by itself allows us to validate the well-formedness of OCL text. We do this by creating a Query instance which automatically validates itself when it is constructed:

boolean valid;

try {
    Query query = QueryFactory.eINSTANCE.createQuery(
        "self.books->collect(b : Book | b.category)->asSet()",
        LibraryPackage.eINSTANCE.getWriter());
    
    // record success
    valid = true;
} catch (IllegalArgumentException e) {
    // record failure to parse
    valid = false;
    System.err.println(e.getLocalizedMessage());
}

The example above parses an expression that computes the distinct categories of Books associated with a Writer. The possible reasons why it would fail to parse (in which case an IllegalArgumentException is thrown) include:

[back to top]

Evaluating OCL Expressions

More interesting than validating an OCL expression is evaluating it on some object. The Query interface provides two methods for evaluating expressions:

In order to support the allInstances() operation on OCL types, the Query API provides the setExtentMap(Map extentMap) method. This assigns a mapping of EClasses to the sets of their instances. We will use this in evaluating a query expression that finds books that have the same title as a designated book:

Map extents = new HashMap();
Set books = new HashSet();
extents.put(LibraryPackage.eINSTANCE.getBook(), books);

Book myBook = Factory.eINSTANCE.createBook();
myBook.setTitle("David Copperfield");
books.add(myBook);

Book aBook = Factory.eINSTANCE.createBook();
aBook.setTitle("The Pickwick Papers");
books.add(aBook);
aBook = Factory.eINSTANCE.createBook();
aBook.setTitle("David Copperfield");
books.add(aBook);
aBook = Factory.eINSTANCE.createBook();
aBook.setTitle("Nicholas Nickleby");
books.add(aBook);

Query query = QueryFactory.eINSTANCE.createQuery(
    "Book.allInstances()->select(b : Book | b <> self and b.title = self.title)",
    LibraryPackage.eINSTANCE.getBook());

query.setExtentMap(extents);

Collection result = query.evaluate(myBook);
System.out.println(result);

Now, let's imagine the confusion that arises from a library that has more than one book of the same title (we are not intending to model copies). We will create an invariant constraint for Books stipulating that this is not permitted, and use the check() method to assert it. Using the myBook and extents map from above:

Query query = QueryFactory.eINSTANCE.createQuery(
    "Book.allInstances()->select(b : Book | b <> self and b.title = self.title)->isEmpty()",
    LibraryPackage.eINSTANCE.getBook());

query.setExtentMap(extents);

boolean result = query.check(myBook);
System.out.println(result);

The difference here is the ->isEmpty() expression that changes our query to a boolean-valued constraint. myBook checks false.

[back to top]

Working with the AST

The OCL Interpreter models the OCL language using EMF. Thus, the AST that results from parsing text is actually an EMF model in its own right.

By implementing the Visitor interface, we can walk the AST of an OCL expression to transform it in some way. This is exactly what the interpreter, itself, does when evaluating an expression: it just walks the expression using an evaluation visitor. For example, we can count the number times that a specific attribute is referenced in the expression:

Query query = QueryFactory.eINSTANCE.createQuery(
    "Book.allInstances()->select(b : Book | b <> self and b.title = self.title)->isEmpty()",
    LibraryPackage.eINSTANCE.getBook());
OclExpression expr = query.getExpression();

AttributeCounter visitor = new AttributeCounter(
    LibraryPackage.eINSTANCE.getBook_Title());
expr.accept(visitor);

System.out.println(
    "Number of accesses to the 'Book::title' attribute: " + visitor.getCount());
where the visitor is defined thus:
class AttributeCounter implements Visitor {
    private final EAttribute attribute;
    private int count = 0;
    
    AttributeCounter(EAttribute attribute) {
        this.attribute = attribute;
    }
    
    int getCount() {
        return count;
    }
    
    public Object visitPropertyCallExp(PropertyCallExp pc) {
        if (pc.getReferredProperty() == attribute) {
            // count one
            count++;
        }
        
        return null;
    }
    
    // other visitor methods are no-ops ...

Because the OCL expression AST is a graph of EMF objects, we can serialize it to an XMI file and deserialize it again later. To save our example expression:

Query query = QueryFactory.eINSTANCE.createQuery(
    "Book.allInstances()->select(b : Book | b <> self and b.title = self.title)->isEmpty()",
    LibraryPackage.eINSTANCE.getBook());
OclExpression expr = query.getExpression();

OclResource res = new OclResource(URI.createFileURI("C:\\temp\\expr.xmi"));
res.setOclExpression(expr);
res.save(Collections.EMPTY_MAP);

To load a saved OCL expression is just as easy:

OclResource res = new OclResource(URI.createFileURI("C:\\temp\\expr.xmi"));
res.load(Collections.EMPTY_MAP);

Query query = QueryFactory.eINSTANCE.createQuery(
    res.getOclExpression());

System.out.println(query.check(myBook));

Defining the OclResource implementation is fairly straightforward. Because the AST actually comprises multiple distinct EObject trees, we must take care to find all referenced elements and include them in the resource, otherwise we will lose data:

public class OclResource extends XMIResourceImpl {

    public OclResource(URI uri) {
        super(uri);
    }
    
    public void setOclExpression(OclExpression expr) {
        getContents().clear();  // clear any previous contents
        getContents().add(expr);
        
        addAllDetachedObjects();  // find detached objects and attach them
    }
    
    public OclExpression getOclExpression() {
        OclExpression result = null;
        
        if (!getContents().isEmpty()) {
            result = (OclExpression) getContents().get(0);
        }
        
        return result;
    }

The addAllDetachedObjects() method uses EMF's cross-referencing feature together with the EcoreUtil API to iterate the content tree searching for referenced objects that are not attached to the resource. This process is repeated on each detached tree until no more detached elements can be found.

    private void addAllDetachedObjects() {
        List toProcess = Collections.singletonList(getOclExpression());
        
        while (!toProcess.isEmpty()) {
            List detachedFound = new ArrayList();
            
            for (Iterator tree = EcoreUtil.getAllContents(toProcess); tree.hasNext();) {
                EObject next = (EObject) tree.next();
                
                for (Iterator xrefs = next.eCrossReferences().iterator(); xrefs.hasNext();) {
                    EObject xref = (EObject) xrefs.next();
                    
                    if (xref.eResource() == null) {
                        // get the root container so that we may attach the entire
                        //    contents of this detached tree
                        xref = EcoreUtil.getRootContainer(xref);
                        
                        detachedFound.add(xref);
                        
                        // attach it to me
                        getContents().add(xref);
                    }
                }
            }
            
            toProcess = detachedFound;
        }
    }

[back to top]

Summary

To illustrate how to work with the OCL Interpreter, we

  1. Parsed and validated OCL expressions.
  2. Evaluated OCL query expressions and constraints.
  3. Transformed an OCL expression AST using the Visitor pattern.
  4. Saved and loaded OCL expressions to/from XMI resources.

[back to top]


Copyright (c) 2000,2005 IBM Corporation and others. All Rights Reserved.