站内搜索: 请输入搜索关键词

当前页面: 开发资料首页Eclipse 英文资料Implementing Model Integrity in EMF with EMFT OCL

Implementing Model Integrity in EMF with EMFT OCL

摘要: This article illustrates how the EMFT OCL parser/interpreter technology adds to the value of EMF/JET code generation as a foundation for model-driven development (MDD). We will see, with fully functional examples, how a metamodel can be generated from an Ecore model without requiring any post-generation custom code, including complete implementations of: invariant constraints derived attributes and references operations By Christian W. Damus, IBM Rational (Canada)

The Goal

First, let's have a look at what the result of our endeavour should look like.

Our abstract goal is to make it easy for the modeler/developer to ensure model integrity. In data modeling, data integrity is typically achieved by two different mechanisms: integrity checks specified as invariant constraints and elimination of redundant data by deriving calculable values. The former has a reactive nature and the latter a more proactive one. Happily, OCL is well suited to the specification of both constraints and derived values (as properties and operations), being a simple but powerful expression language designed to be embedded in UML models (and models of related languages such as Ecore).

Our concrete goal in this article is to generate a complete metamodel implementation for the following Ecore model, without having to fill in any TODOs with custom code:

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 1 The Employee model from which we will generate our code </table>

When we have finished, our Ecore model will include annotations to provide OCL specifications of invariant constraints, derived properties, and operations. The generated model classes will have methods implementing these as described in the next few sections.

Invariant Constraints

The EMF Validation Framework prescribes the form of invariant constraints: boolean-valued operations with a DiagnosticChain and a Map as input parameters. Violation of a constraint adds a Diagnostic to the chain and results in a false return value.

Before OCL ...

Without EMFT OCL, our generated code for invariant constraints is incomplete: we have to replace a TODO comment () with some useful code and remember to mark the method as not generated:

/** * * * @generated */ public boolean validateEmployees(DiagnosticChain diagnostics, Map context) { // TODO: implement this method // -> specify the condition that violates the invariant // -> verify the details of the diagnostic, including severity and message // Ensure that you remove @generated or mark it @generated NOT if (false) { if (diagnostics != null) { diagnostics.add (new BasicDiagnostic (Diagnostic.ERROR, EmployeeValidator.DIAGNOSTIC_SOURCE, EmployeeValidator.DEPARTMENT__VALIDATE_EMPLOYEES, EcorePlugin.INSTANCE.getString("_UI_GenericInvariant_diagnostic", new Object[] { "validateEmployees", EObjectValidator.getObjectLabel(this, context) }), //$NON-NLS-1$ //$NON-NLS-2$ new Object [] { this })); } return false; } return true; }

After OCL ...

The following example shows the desired implementation of the constraint on departments stipulating that a Department that has a manager must also have one or more employees. Note that the OCL expression of this constraint is stored in the EMF metadata; it is not manifest in the Java™ code at all. Thus, changing the constraint definition and re-testing doesn't even require that we regenerate the code (if we use the GenModel option to initialize the EPackage from the Ecore model at run-time).

/** * * * @generated */ public boolean validateEmployees(DiagnosticChain diagnostics, Map context) { if (validateEmployeesInvOCL == null) { EOperation eOperation = (EOperation) eClass().getEOperations().get(0); Environment env = ExpressionsUtil.createClassifierContext(eClass()); EAnnotation ocl = eOperation.getEAnnotation(OCL_ANNOTATION_SOURCE); String body = (String) ocl.getDetails().get("invariant"); //$NON-NLS-1$ try { validateEmployeesInvOCL = ExpressionsUtil.createInvariant(env, body, true); } catch (ParserException e) { throw new UnsupportedOperationException(e.getLocalizedMessage()); } } Query query = QueryFactory.eINSTANCE.createQuery(validateEmployeesInvOCL); EvalEnvironment evalEnv = new EvalEnvironment(); query.setEvaluationEnvironment(evalEnv); if (!query.check(this)) { if (diagnostics != null) { diagnostics.add (new BasicDiagnostic (Diagnostic.ERROR, EmployeeValidator.DIAGNOSTIC_SOURCE, EmployeeValidator.DEPARTMENT__VALIDATE_EMPLOYEES, EcorePlugin.INSTANCE.getString("_UI_GenericInvariant_diagnostic", new Object[] { "validateEmployees", EObjectValidator.getObjectLabel(this, context) }), //$NON-NLS-1$ //$NON-NLS-2$ new Object [] { this })); } return false; } return true; }

Our validation method will start by checking () whether we have previously parsed and cached the OCL constraint expression. If not, then we prepare an OCL parsing environment at and obtain the constraint expression from an annotation on the EOperation at . The OCL constraint expression is parsed in the classifier context at and cached so that it will not have to be parsed again.

Once we have our parsed OCL constraint expression, we construct an executable Query for it () and check whether this object satisfies the constraint (). Java™'s this reference is bound to OCL's self variable.

Derived Properties

EMF implements derived properties as structural features that are marked as transient (not persisted) and volatile (no storage is allocated). Usually, they are also not changeable.

Before OCL ...

Again, EMF's default code generation requires us to complete the implementation of the derivation and to protect our hand-written code from being overwritten the next time that we generate. The starting point of this process is:

/** * * * @generated */ public EList getEmployees() { // TODO: implement this method to return the 'Employees' reference list // Ensure that you remove @generated or mark it @generated NOT throw new UnsupportedOperationException(); }

After OCL ...

Once again, EMFT OCL can do all of the heavy lifting for us:

/** * * * @generated */ public EList getEmployees() { EStructuralFeature eFeature = (EStructuralFeature) eClass().getEStructuralFeatures().get(3); if (employeesDeriveOCL == null) { Environment env = ExpressionsUtil.createPropertyContext(eClass(), eFeature); EAnnotation ocl = eFeature.getEAnnotation(OCL_ANNOTATION_SOURCE); String derive = (String) ocl.getDetails().get("derive"); //$NON-NLS-1$ try { employeesDeriveOCL = ExpressionsUtil.createQuery(env, derive, true); } catch (ParserException e) { throw new UnsupportedOperationException(e.getLocalizedMessage()); } } Query query = QueryFactory.eINSTANCE.createQuery(employeesDeriveOCL); EvalEnvironment evalEnv = new EvalEnvironment(); query.setEvaluationEnvironment(evalEnv); Collection result = (Collection) query.evaluate(this); return new EcoreEList.UnmodifiableEList(this, eFeature, result.size(), result.toArray()); }

This method is just like the constraint, except in a few details. First, as this is a property derivation, the OCL context is the structural feature (), not the classifier. Also, since it is not a constraint, we parse the OCL as a query (), which is not required to be a boolean-valued expression. Finally, queries are evaluated (), not checked, returning a result conformant to the declared property type. Our template will have to account for multi-valued and scalar properties of both reference and primitive types, taking care that ELists implement the InternalEList interface as expected by much of the EMF machinery.

Operations

OCL is often used to specify operation precondition and postcondition constraints. A third kind of OCL expression defined on operations is the body expression, which defines the value of the operation in terms of its parameters and the properties of the context classifier.

Before OCL ...

At the risk of being too repetitive, let us see what EMF generates by default for the implementation of EOperations:

/** * * * @generated */ public boolean reportsTo(Employee mgr) { // TODO: implement this method // Ensure that you remove @generated or mark it @generated NOT throw new UnsupportedOperationException(); }

After OCL ...

Here, our OCL-based code is just a little more elaborate (complicated by the fact of operations having parameters):

/** * * * @generated */ public boolean reportsTo(Employee mgr) { if (reportsToBodyOCL == null) { EOperation eOperation = (EOperation) eClass().getEOperations().get(2); Environment env = ExpressionsUtil.createOperationContext(eClass(), eOperation); EAnnotation ocl = eOperation.getEAnnotation(OCL_ANNOTATION_SOURCE); String body = (String) ocl.getDetails().get("body"); //$NON-NLS-1$ try { reportsToBodyOCL = ExpressionsUtil.createQuery(env, body, true); } catch (ParserException e) { throw new UnsupportedOperationException(e.getLocalizedMessage()); } } Query query = QueryFactory.eINSTANCE.createQuery(reportsToBodyOCL); EvalEnvironment evalEnv = new EvalEnvironment(); evalEnv.add("mgr", mgr); //$NON-NLS-1$ query.setEvaluationEnvironment(evalEnv); return ((Boolean) query.evaluate(this)).booleanValue(); }

Again this method is very similar to the accessor for the derived property illustrated above. The chief distinction is that the context () of the OCL expression is an operation, which ensures that the names and types of the parameters (if any) are known to the OCL parser. When invoking the operation, these parameter variables are bound () to the actual argument values. Finally, although this is not particular to operations (versus derived properties), the result in this case () is a primitive type whereas previously we saw a reference collection type.

OCL, by design, is a side-effect-free language. This means, in particular, that an OCL expression cannot modify any elements of the model, nor even temporary objects that it creates (such as strings, collections, and tuples). However, we can use OCL to implement operations that modify the properties of the receiver using OCL's Tuple types. A tuple is a set of name-value pairs (called "tuple parts"), and given an expression that results in a tuple, generated code could assign the tuple's values to the properties corresponding to their names. If the tuple has a result part, this could even be used as a return value for the operation.

Prerequisites

In addition to the latest available stable EMF build (version 2.2 or later), we will be using the EMFT OCL project to parse and interpret OCL expressions, so our generated code will have an additional dependency on the org.eclipse.emf.ocl plug-in.

Next, we create our EMF project. I named it org.eclipse.emf.ocl.examples.codegen; the name does not matter. The basic non-UI plug-in template is sufficient, but we will add a model/ and a templates/ folder to it. The former will contain our Ecore and code-gen models, the latter our custom JET templates.

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 2 The EMF project layout </table>

Now, we create the Employee model. Save the Employee.ecore file in your model/ folder and open it.

Create the Employee.genmodel file from this model: select the Employee.ecore file and choose "File -> New -> Other...", taking the "EMF Modeling Framework / EMF Model" option.

In the genmodel editor, enable dynamic generation templates and specify the templates/ directory as shown here:

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 3 Genmodel configuration for dynamic templates </table>

Because our generated code requires the EMFT OCL project, we also add the org.eclipse.emf.ocl plug-in as a Model Plug-in Variable.

No other changes to the genmodel are required. The rest of our work is in the templates (and in the OCL specifications in the Ecore model).

The Templates

In this section, we will explore (in not too great detail) the templates that will generate the code that we specified above. The templates are necessarily ugly to look at and contain a good deal of content unrelated to the task at hand. Therefore, this article will only highlight some of the more salient bits. For the complete text, see the accompanying example source project.

If you thought that you didn't know JET but the syntax of these templates looks familiar to you, then you are probably versant in JSP (Java Server Pages™) technology. The JET syntax is very similar, and the template compilation is essentially the same, even to compiling the resulting sources and dynamically loading the binary output into the host VM.

If you are not familiar with either JET or JSP, the EMF home page has plenty of documentation to get you started. See below for some references. Basically, there are three key things to know:

We will also encounter a couple of convenient constructs in EMF's templates that greatly simplify customization and promote re-use by letting our custom templates define only what's different in our system from base EMF. These are insertion and override points.

An insertion point is indicated by an <%@include%> directive with a relative path to a template fragment that should be inserted at this point. The inclusion has a fail="silent" attribute instructing JET to just proceed if the include cannot be found. EMF has identified a number of such anchor points where it would be useful to inject custom code.

An overrideable block is indicated by an <%@include%> directive with a relative path to a template fragment that should replace all of the content from a <%@start%> directive to the following <%@end%>. An override has a fail="alternative" attribute which instructs JET to just process everything between the start and end if the override cannot be found.

Basic Templates

We will be customizing only the implementation classes of our metamodel types. The Class.javajet template has a wealth of override and insertion points for us to work with, so our class template will be very simple (the authors of this template anticipated where we might want to make customizations). The templates/model/Class.javajet file looks like:

<%@ jet package="org.eclipse.emf.ocl.examples.codegen.templates.model" imports="java.util.* org.eclipse.emf.codegen.ecore.genmodel.* org.eclipse.emf.ecore.*" class="Class" version="$Id: article.html,v 1.1 2006/08/01 17:47:33 wbeaton Exp $" %> <% final String oclNsURI = "http://www.eclipse.org/OCL/examples/ocl"; %> <%@ include file="Class.javajet"%>

The first line declares a package for our custom template and imports. More interesting is , where we define a constant that all of our other templates will use: the source identifying the annotations containing the OCL specifications of our metamodel. The last line () includes the default template. Our other templates will override or insert at various points in this template.

Generated Fields

We want to cache parsed OCL expressions in static fields of the generated implementation classes. EMF's default templates provide a convenient insertion point for additional fields: the templates/model/Class/declaredFieldGenFeature.insert.javajetinc file:

<%if (isImplementation) { boolean hasOCL = false;%> <%for (Iterator i=genClass.getImplementedGenOperations().iterator(); i.hasNext();) { GenOperation genOperation = (GenOperation) i.next(); String body = null; EAnnotation ocl = genOperation.getEcoreOperation().getEAnnotation(oclNsURI); if (ocl != null) body = (String) ocl.getDetails().get("body"); if (body != null) { hasOCL = true;%> /** * The parsed OCL expression for the body of the '{@link #<%=genOperation.getName()%> <%=genOperation.getFormattedName()%>}' operation. * * * @see #<%=genOperation.getName()%> * @generated */ private static <%=genModel.getImportedName("org.eclipse.emf.ocl.expressions.OCLExpression")%> <%=genOperation.getName()%>BodyOCL; <%} // ... and similarly if the GenOperation is an invariant constraint ... } // end for all GenOperations for (Iterator i=genClass.getImplementedGenFeatures().iterator(); i.hasNext();) { // ... similar processing as for GenOperations ... } if (hasOCL) { %> private static final String OCL_ANNOTATION_SOURCE = "<%=oclNsURI%>";<%=genModel.getNonNLS()%> <% } }%>

First, we loop () through the class's GenOperations, generating a static field () of type OCLExpression for each operation that has a body expression or an invariant constraint annotation (using the constant defined in our Class.javajet template, above). We also do essentially the same () for all GenFeatures, looking for derive annotations. Finally, if after all of this looping we have generated at least one OCL expression field, we also emit a manifest constant for the OCL annotation source, which is used in the generated methods.

Operation Template

As an example let us consider the template for the OCL-specified operations. The templates for derived properties and for invariant constraints will be similar (the latter includes, additionally, the usual code appending a diagnostic to the DiagnosticChain).

The following is the content of the templates/model/Class/implementedGenOperation.TODO.override.javajetinc file. This is an optional template that overrides the default // TODO comment in generated EOperations:

<% String body = null; EOperation eOperation = genOperation.getEcoreOperation(); EAnnotation ocl = eOperation.getEAnnotation(oclNsURI); if (ocl != null) body = (String) ocl.getDetails().get("body"); if (body == null) { %> // TODO: implement this method // Ensure that you remove @generated or mark it @generated NOT throw new UnsupportedOperationException(); <% } else { final String expr = genOperation.getName() + "BodyOCL"; %> if (<%=expr%> == null) { <%=genModel.getImportedName("org.eclipse.emf.ecore.EOperation")%> eOperation = (<%=genModel.getImportedName("org.eclipse.emf.ecore.EOperation")%>) eClass().getEOperations().get(<%=eOperation.getEContainingClass().getEOperations().indexOf(eOperation)%>); <%=genModel.getImportedName("org.eclipse.emf.ocl.parser.Environment")%> env = <%=genModel.getImportedName("org.eclipse.emf.ocl.expressions.util.ExpressionsUtil")%>.createOperationContext(eClass(), eOperation); <%=genModel.getImportedName("org.eclipse.emf.ecore.EAnnotation")%> ocl = eOperation.getEAnnotation(OCL_ANNOTATION_SOURCE); String body = (String) ocl.getDetails().get("body");<%=genModel.getNonNLS()%> try { <%=expr%> = <%=genModel.getImportedName("org.eclipse.emf.ocl.expressions.util.ExpressionsUtil")%>.createQuery(env, body, true); } catch (<%=genModel.getImportedName("org.eclipse.emf.ocl.parser.ParserException")%> e) { throw new UnsupportedOperationException(e.getLocalizedMessage()); } } <%=genModel.getImportedName("org.eclipse.emf.ocl.query.Query")%> query = <%=genModel.getImportedName("org.eclipse.emf.ocl.query.QueryFactory")%>.eINSTANCE.createQuery(<%=expr%>); <%=genModel.getImportedName("org.eclipse.emf.ocl.expressions.util.EvalEnvironment")%> evalEnv = new <%=genModel.getImportedName("org.eclipse.emf.ocl.expressions.util.EvalEnvironment")%>(); <% for (Iterator iter = genOperation.getEcoreOperation().getEParameters().iterator(); iter.hasNext();) { EParameter param = (EParameter) iter.next(); %> evalEnv.add("<%=param.getName()%>", <%=param.getName()%>);<%=genModel.getNonNLS()%> <% } %> query.setEvaluationEnvironment(evalEnv); <% if (genOperation.isListType()) { %> <%=genModel.getImportedName("java.util.Collection")%> result = (<%=genModel.getImportedName("java.util.Collection")%>) query.evaluate(this); return new <%=genModel.getImportedName("org.eclipse.emf.common.util.BasicEList")%>.UnmodifiableEList(result.size(), result.toArray()); <% } else if (genOperation.isPrimitiveType()) { %> return ((<%=genOperation.getObjectType()%>) query.evaluate(this)).<%=genOperation.getPrimitiveValueFunction()%>(); <% } else { %> return (<%=genOperation.getImportedType()%>) query.evaluate(this); <% } %> <% } %>

First, we look for an OCL annotation with a body expression (). If we do not find one, then we emit the default comment () and are done. Otherwise, we continue by generating the lazy initialization () of the static OCL expression, followed by the code () that constructs the query and evaluation environment.

At , we loop through the operation's parameters to generate the argument bindings in the evaluation environment. At is the evaluation of the OCL body.

Finally, at , we examine the result type of the operation and determine what kind of return statement to generate. The operation may be multi-valued, in which case the result is an EList type. Otherwise, it is a scalar which may be a primitive type, which OCL returns as a wrapper object. Note that a more robust implementation would have to consider also the possibility that the genmodel specifies that multi-valued features use array types rather than lists.

Source Code

To run the full example or simply to view the source code, unzip ocl-codegen.zip into your workspace. Or, better yet, use the Eclipse Project Import wizard to import the ZIP as a project.

Then, open the model/employee.genmodel file and invoke "Generate All" to see the code that is generated for the OCL-specified features. Launch a run-time workbench to create Employee models, validate them, and perform queries using the Interactive OCL Console.

The following figure shows an example Employee model with an employee selected who has a manager associated with a department and is also a manager of others. The Department and Is Manager properties are derived.

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 4 An Employee model showing derived properties </table>

The Interactive OCL Console can be used to exercise the operations. In this example, we find all of the "reports to (directly or indirectly)" relationships among managers in the Acme company:

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 5 A query utilizing the OCL-specified operations and properties </table>

This query accesses the derived isManager property and calls the reportsTo(Employee) operation, both of which we implemented using OCL. It is worth noting here that OCL encourages the definition of additional properties and operations externally to the model, as conveniences for the formulation of constraints. The EMFT OCL implementation supports these "def:" expressions via the ExpressionsUtil.define() and IOCLHelper.define() APIs.

Figure 6 illustrates a violation of our example constraint, detected by the validator. The "OCL Development" department has a manager that is not really a manager, because it has no reports (the Employee.isManager property is derived from the existence of directReports). This is not a valid department according to our example constraint, as it results in a department having a manager but no employees (the latter being derived, again via OCL, from the manager's reports).

<table border="0" cellpadding="8"> <tr><td></td></tr> Figure 6 Validation problem reporting an OCL constraint violation </table>

When an Ecore model contains EOperations defining invariant constraints, the EMF code generator creates an EValidator implementation for the package. This validator is registered against the generated EPackage in the EValidator.Registry, where it is found by the "Validate" context menu action and invoked iteratively on the elements of an instance model.

Conclusion

We have seen how, using the EMFT OCL technology, we can use OCL to quickly and easily generate a complete metamodel, with

We used the EMF GenModel's dynamic template support to extend the code generation system. A more complete implementation would use the new (in EMF 2.2) adapter-based generator extensibility framework with statically compiled templates to transform OCL expressions into Java™ for maximal efficiency. For the subset of OCL for which EMFT OCL provides evaluation support, transformation to Java™ would not be very difficult and could even surpass EMFT's evaluation capabilities (e.g., by implementing @pre expressions in operation postcondition constraints).

References

EMF Code Generation
Advanced Features of the Eclipse Modeling Framework (EclipseCon tutorial)
Other JET-related tutorials
Extensible Code Generator (Bug 75925)
EMF Validation Framework Overview
EMFT
OCL SDK Download (includes on-line Developer Guide and Interactive OCL Console example)
Validation SDK Download (includes on-line Developer Guide and EValidator adapter example)
OCL 2.0
Specification

Acknowledgments

The author would like to thank Frédéric Plante, Ed Merks, and Richard Gronback for their helpful editorial suggestions.

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.
IBM is a registered trademark of International Business Machines Corporation in the United States, other countries, or both.
Other company, product, or service names may be trademarks or service marks of others.
↑返回目录
前一篇: Custom Drawing Table and Tree Items
后一篇: Designing Accessible Applications in Eclipse