This tutorial illustrates the various services provided by the Classic Eclipse OCL implementation.
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:
For an environment in which to test the OCL expressions that you will create in this tutorial, install the Library Metamodel example.
OCL 2.0 specification.
The first responsibility of the OCL interpreter is to parse OCL expressions. One of the purposes of parsing an expression is to validate it: if it can be parsed, it is well-formed (the parser automatically validates the expression against the semantic well-formedness rules).
The main entrypoint into the OCL API is the
OCL class. An
OCL
provides an autonomous OCL parsing environment. It tracks all constraints that are parsed in this environment, including the definitions of additional operations and attributes. The
OCL.newInstance()
factory method is used to create a new OCL with an
EnvironmentFactory
that provides the binding to a particular metamodel (Ecore or UML). In this tutorial, we will use the Ecore binding.
To parse a query expression, we will use the
OCLHelper
object, which provides convenient operations for parsing queries and constraints
(intended for processing constraints embedded in models).
boolean valid;
OCLExpression<EClassifier> query = null;
try {
// create an OCL instance for Ecore
OCL<?, EClassifier, ?, ?, ?, ?, ?, ?, ?, Constraint, EClass, EObject> ocl;
ocl = OCL.newInstance(EcoreEnvironmentFactory.INSTANCE);
// create an OCL helper object
OCLHelper<EClassifier, ?, ?, Constraint> helper = ocl.createOCLHelper();
// set the OCL context classifier
helper.setContext(EXTLibraryPackage.Literals.WRITER);
query = helper.createQuery("self.books->collect(b : Book | b.category)->asSet()");
// record success
valid = true;
} catch (ParserException e) {
// record failure to parse
valid = false;
System.err.println(e.getLocalizedMessage());
}
The example above parses an expression that computes the distinct categories
of
Book
s associated with a
Writer
. The possible
reasons why it would fail to parse (in which case a
ParserException
is thrown) include:
syntactical problems: misplaced or missing constructs such as closing
parentheses, variable declarations, type expressions, etc.
semantic problems: unknown attributes or operations of the context
type or referenced types, unknown packages, classes, etc.
OCL is primarily intended for the specification of
constraint s. Unlike
queries, there are a variety of different kinds of constraints used in different
places in a model. These include classifier invariants, operation constraints,
and attribute derivation constraints. The
OCLHelper
can parse these for us.
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 @Book@s stipulating that this is not permitted:
Constraint invariant = null;
try {
// set the OCL context classifier
helper.setContext(EXTLibraryPackage.Literals.LIBRARY);
invariant = helper.createInvariant(
"Library.allInstances()->forAll(b1, b2 | b1 <> b2 implies b1.title <> b2.title)");
} catch (ParserException e) {
// record failure to parse
System.err.println(e.getLocalizedMessage());
}
Parsing constraints differs from parsing query expressions because they have
additional well-formedness rules that the parser checks. For example, an
invariant constraint must be boolean-valued, an attribute derivation constraint
must conform to the type of the attribute, and such constructs as @pre
and
oclIsNew()
may only be used in operation post-condition constraints.
More interesting than parsing an OCL expression or constraint is evaluating it
on some object. The
Query
interface provides two methods for evaluating expressions. Queries are
constructed by factory methods on the
OCL
class.
evaluates the expression on the specified object, returning the result.
The caller is expected to know the result type, which could be a
primitive,
EObject
, or a collection. There
are variants of this method for evaluation of the query on multiple
objects and on no object at all (for queries that require no "self"
context).
This method evaluates a special kind of OCL expression called a constraint. Constraints are distinguished from other OCL queries by having a boolean value; thus, they can be used to implement invariant or pre/post-condition constraints. There are variants for checking multiple objects and for selecting/rejecting elements of a list that satisfy the constraint.
In order to support the
allInstances()
operation on OCL types,
the
OCL
API provides the
setExtentMap(Map<CLS, ? extends Set<? extends E>> extentMap)
method. This assigns a mapping of classes (in the Ecore binding,
EClass
es) to the sets of their instances. By default,
the
OCL
provides a dynamic map that computes the
extents on demand from the contents of a
Resource
.
An alternative extent map can be
found in
org.eclipse.ocl.ecore.opposites.ExtentMap
.
We will use a custom extent map in evaluating a query expression that finds
books that have the same title as a designated book:
// create an extent map
Map<EClass, Set<? extends EObject>> extents = new HashMap<EClass, Set<? extends EObject>>();
Set<Book> books = new HashSet<Book>();
extents.put(EXTLibraryPackage.Literals.BOOK, books);
// tell the OCL environment what our classifier extents are
ocl.setExtentMap(extents);
Library library = EXTLibraryFactory.eINSTANCE.createLibrary();
Book myBook = EXTLibraryFactory.eINSTANCE.createBook();
myBook.setTitle("David Copperfield");
books.add(myBook);
// this book is in our library
library.add(myBook);
Writer dickens = EXTLibraryFactory.eINSTANCE.createWriter();
dickens.setName("Charles Dickens");
Book aBook = EXTLibraryFactory.eINSTANCE.createBook();
aBook.setTitle("The Pickwick Papers");
aBook.setCategory(BookCategory.MYSTERY_LITERAL);
books.add(aBook);
aBook = EXTLibraryFactory.eINSTANCE.createBook();
aBook.setTitle("David Copperfield");
aBook.setCategory(BookCategory.BIOGRAPHY_LITERAL); // not actually, of course!
books.add(aBook);
aBook = EXTLibraryFactory.eINSTANCE.createBook();
aBook.setTitle("Nicholas Nickleby");
aBook.setCategory(BookCategory.BIOGRAPHY_LITERAL); // not really
books.add(aBook);
dickens.addAll(books); // Dickens wrote these books
library.addAll(books); // and they are all in our library
// use the query expression parsed before to create a Query
Query<EClassifier, EClass, EObject> eval = ocl.createQuery(query);
Collection<?> result = (Collection<?>) eval.evaluate(dickens);
System.out.println(result);
The same
Query
API is used to check constraints.
Using the
library
and
extents
map from above and the
constraint parsed previously:
eval = ocl.createQuery(constraint);
boolean ok = eval.check(library);
System.out.println(ok);
The
OCLHelper
interface provides an operation that
computes content-assist proposals in an abstract form, as
Choice
s.
An application’s UI can then convert these to JFace’s
ICompletionProposal
type.
Obtaining completion choices consists of supplying a partial OCL expression
(up to the cursor location in the UI editor) to the
OCLHelper::getSyntaxHelp(ConstraintKind, String)
, java.lang.String)
method. This method requires a
ConstraintKind
enumeration indicating the type of constraint that is to be parsed (some OCL
constructs are restricted in the kinds of constraints in which they may be used).
helper.setContext(EXTLibraryPackage.Literals.BOOK);
List<Choice> choices = helper.getSyntaxHelp(
ConstraintKind.INVARIANT,
"Book.allInstances()->excluding(self).");
for (Choice next : choices) {
switch (next.getKind()) {
case OPERATION:
case SIGNAL:
// the description is already complete
System.out.println(next.getDescription());
case PROPERTY:
case ENUMERATION_LITERAL:
case VARIABLE:
System.out.println(next.getName() + " : " + next.getDescription();
break;
default:
System.out.println(next.getName());
break;
}
}
A sample of the output looks like:
author : Writer
title : String
oclIsKindOf(typespec : OclType)
oclAsType(typespec : OclType) : T
...
The choices also provide the model element that they represent, from which a more sophisticated application can construct appropriate JFace completions, including context information, documentation, etc.
The OCL Interpreter models the OCL language using EMF’s Ecore with support for Java-style generic types. The bindings of this generic Abstract Syntax Model for Ecore and for UML substitutes these metamodels' constructs for the generic type parameters, plugging in the definitions of the “classifier”, “operation”, “constraint”, etc. constructs of the OCL vocabulary. These bindings, then, support persistence in or as an adjunct to Ecore and UML models.
For processing the abstract syntax tree (AST) parsed from OCL text, the API
supplies a
Visitor
interface. By implementing this interface (or extending the
AbstractVisitor
class, which is recommended), we can walk the AST of an OCL expression to
transform it in some way.
This is exactly what the interpreter, itself, does to evaluate 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 an expression:
helper.setContext(EXTLibraryPackage.Literals.BOOK);
OCLExpression<EClassifier> query = helper.parseQuery(
"Book.allInstances()->select(b : Book | b <> self and b.title = self.title)");
AttributeCounter visitor = new AttributeCounter(
EXTLibraryPackage.Literals.BOOK__TITLE);
System.out.println(
"Number of accesses to the 'Book::title' attribute: " + query.accept(visitor));
where the visitor is defined thus:
class AttributeCounter extends AbstractVisitor<Integer,
EClassifier, EOperation, EStructuralFeature, EEnumLiteral,
EParameter, EObject, EObject, EObject, Constraint> {
private final EAttribute attribute;
AttributeCounter(EAttribute attribute) {
super(0); // initialize the result of the AST visitiation to zero
this.attribute = attribute;
}
protected Integer handlePropertyCallExp(PropertyCallExp<EClassifier, EStructuralFeature> callExp,
Integer sourceResult, List<Integer> sourceResults) {
if (callExp.getReferredProperty() == attribute) {
// count one
result++;
}
return result;
}
}
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,
we start over by initializing our
OCL
instance with
a resource in which it will persist the environment and in which we will
persist the parsed expression. The key is in the persistence of the
environment: OCL defines a variety of classes on the fly by template
instantiation. These include collection types, tuple types, and message types.
Other elements needing to be persisted are additional operations and attributes
that may be defined in the local environment.
// create a resource in which to store our parsed OCL expressions and constraints
Resource res = resourceSet.createResource(
URI.createPlatformResourceURI("/MyProject/myOcl.xmi", true);
// initialize a new OCL environment, persisted in this resource
ocl = OCL.newInstance(EcoreEnvironmentFactory.INSTANCE, res);
// for the new OCL environment, create a new helper
helper = OCL.createOCLHelper();
helper.setContext(EXTLibraryPackage.Literals.BOOK);
// try a very simple expression
OCLExpression<EClassifier> query = helper.createQuery("self.title");
// store our query in this resource. All of its necessary environment has
// already been stored, so we insert the query as the first resource root
res.getContents().add(0, query);
res.save(Collections.emptyMap());
res.unload();
To load a saved OCL expression is just as easy:
Resource res = resourceSet.getResource(
URI.createPlatformResourceURI("/MyProject/myOcl.xmi", true),
true;
@SuppressWarnings("unchecked")
OCLExpression<EClassifier> query = (OCLExpression<EClassifier>) res.getContents().get(0);
System.out.println(ocl.evaluate(myBook, query));
In the snippet above, we used the
OCL
's convenience
method for a one-shot evaluation of a query. Looking at the contents of the
XMI document that we saved, we see that the
self
variable declaration is not owned by the query expression, but is, rather,
free-standing. The
ExpressionInOCL
metaclass solves
this problem by providing properties that contain context variable declarations,
including
self
and (in the context of operations)
operation parameters.
<?xml version="1.0" encoding="ASCII"?>
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" xmlns:ocl.ecore="http://www.eclipse.org/ocl/1.1.0/Ecore">
<ocl.ecore:PropertyCallExp xmi:id="_897fVPfmEduCQ48h829a5g">
<eType xsi:type="ocl.ecore:PrimitiveType" href="http://www.eclipse.org/ocl/1.1.0/oclstdlib.ecore#/0/String"/>
<source xsi:type="ocl.ecore:VariableExp" xmi:id="_897fVvfmEduCQ48h829a5g" name="self" referredVariable="_897fUvfmEduCQ48h829a5g">
<eType xsi:type="ecore:EClass" href="http://www.org.eclipse/ocl/examples/library/extlibrary.ecore/1.0.0#//Book"/>
</source>
<referredProperty xsi:type="ecore:EAttribute" href="http://www.org.eclipse/ocl/examples/library/extlibrary.ecore/1.0.0#//Book/title"/>
</ocl.ecore:PropertyCallExp>
<ocl.ecore:Variable xmi:id="_897fUvfmEduCQ48h829a5g" name="self">
<eType xsi:type="ecore:EClass" href="http://www.org.eclipse/ocl/examples/library/extlibrary.ecore/1.0.0#//Book"/>
</ocl.ecore:Variable>
</xmi:XMI>
To illustrate how to work with the OCL API, we
Parsed and validated OCL expressions and constraints.
Evaluated OCL query expressions and constraints.
Obtained content-assist suggestions for the completion of OCL expressions.
Transformed an OCL expression AST using the Visitor pattern.
Saved and loaded OCL expressions to/from XMI resources.