| h2. Customizing the Environment |
| |
| An application that integrates OCL may find it advantageous to provide its users with an |
| enhanced OCL environment, to simplify their task of formulating OCL constraints and queries. |
| For example, an application might define additional "primitive" operations on the OCL |
| standard data types that are pertinent to its domain, or "global" variables that inject |
| useful objects into the user's context. It is also possible to customize the way |
| "hidden" opposites are looked up and navigated, specifically to allow reverse navigation |
| across Ecore references that have no opposite defined. |
| |
| h3. Defining Global Variables |
| |
| Consider an application that allows end-users to specify conditions, using OCL, to filter |
| the objects that are shown in the user interface. Given a sufficiently rich model |
| (expressed in Ecore or UML) of the objects that the UI presents, many conditions can be |
| expressed entirely in terms of this model. However, some queries might depend on state |
| of the application, itself, not the data: which perspective is active, whether some view |
| is showing, or even the time of day. These are not characteristics of the objects that the |
| user wishes to filter. |
| |
| Such an application might, then, choose to define application-specific variables that a |
| filter condition can query: @app$perspective@, @app$views@, @app$time@. Or, perhaps a |
| single variable @app$@, that has properties that a condition can access: |
| |
| bc.. |
| -- filter out OCL files in the Web Development perspective |
| self.extension = 'ocl' and app$.perspective = 'Web Development' |
| p. |
| |
| To do this, we define a small Ecore model of our application context, e.g.: |
| |
| !{width:70%}images/5160-appcontext.png(Application Context Model)! |
| |
| Then, in the code that parses a user's filter condition: |
| |
| bc.. |
| OCL<?, EClassifier, ?, ?, ?, ?, ?, ?, ?, Constraint, EClass, EObject> ocl; |
| ocl = OCL.newInstance(EcoreEnvironmentFactory.INSTANCE); |
| |
| OCLHelper<EClassifier, ?, ?, Constraint> helper = ocl.createOCLHelper(); |
| helper.setContext(MyPackage.Literals.FILE); |
| |
| // create a variable declaring our global application context object |
| Variable<EClassifier, EParameter> appContextVar = |
| ExpressionsFactory.eINSTANCE.createVariable(); |
| appContextVar.setName("app$"); |
| appContextVar.setType(AppPackage.Literals.APP_CONTEXT); |
| |
| // add it to the global OCL environment |
| ocl.getEnvironment().addElement(appContextVar.getName(), appContextVar, true); |
| |
| List<Constraint> conditions = new ArrayList<Constraint>(); |
| |
| // parse the user's filter conditions |
| for (String cond : getFilterConditions()) { |
| conditions.add(helper.createInvariant(cond)); |
| } |
| |
| // apply the filters |
| applyFilters(conditions); |
| p. |
| |
| The body of our hypothetical @applyFilters()@ method must bind this |
| context variable to a value. In this case, the value can be computed when we apply the |
| filters: |
| |
| bc.. |
| AppContext appContext = AppFactory.eINSTANCE.createAppContext(); |
| |
| // hypothetical workbench utilities |
| appContext.setPerspective(WorkbenchUtil.getCurrentPerspective()); |
| appContext.getViews().addAll(WorkbenchUtil.getOpenViewIDs()); |
| appContext.setTime(new Date()); |
| |
| List<Query<EClassifier, EClass, EObject>> queries = |
| new ArrayListlt;Query<EClassifier, EClass, EObject>>(constraints.size()); |
| |
| for (Constraint next : constraints) { |
| Query<EClassifier, EClass, EObject> query = ocl.createQuery(next); |
| |
| // bind the variable value |
| query.getEvaluationEnvironment().add("app$", appContext()); |
| |
| queries.add(query); |
| } |
| |
| filter(queries); // applies these filters to the current objects |
| // by evaluating the OCLS on them. |
| p. |
| |
| h3. Defining Helper Operations in Java |
| |
| OCL allows the definition of additional operations and attributes using @def:@ expressions. This is very convenient for the formulation of constraints, but what if the operation that we need is something like a regex pattern match? |
| |
| bc.. |
| class Person |
| inv valid_ssn: self.ssn.regexMatch('\d{3}-\d{3}-\d{3}') <> null |
| p. |
| |
| We might try to define this using OCL, as an additional operation on the OCL Standard |
| Library's @String@ primitive type: |
| |
| bc.. |
| class String |
| def: regexMatch(pattern : String) : String = |
| -- ??? |
| p. |
| |
| The operations available in the OCL Standard Library simply are not sufficient to express |
| the value of this operation, which should return the substring matching a regex pattern or |
| @null@ if the pattern does not match. We need to implement this |
| operation in Java. We can do that by creating a custom @Environment@ |
| that knows how to look up this operation, and an @EvaluationEnvironment@ |
| that knows how it is implemented. |
| |
| First, let's start by defining a specialization of the |
| "@EcoreEnvironment@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/EcoreEnvironment.html . The constructor that is used to initialize the root environment of an @OCL@ |
| instance will add our @regexMatch@ additional operation to the |
| @String@ primitive type. The constructor that is used to create |
| nested environments copies the operation from its parent. |
| |
| bc.. |
| class MyEnvironment extends EcoreEnvironment { |
| EOperation regexMatch; |
| |
| // this constructor is used to initialize the root environment |
| MyEnvironment(EPackage.Registry registry) { |
| super(registry); |
| |
| defineCustomOperations(); |
| } |
| |
| // this constructor is used to initialize child environments |
| MyEnvironment(MyEnvironment parent) { |
| super(parent); |
| |
| // get the parent's custom operations |
| regexMatch = parent.regexMatch; |
| } |
| |
| // override this to provide visibility of the inherited protected method |
| @Override |
| protected void setFactory( |
| EnvironmentFactory<EPackage, EClassifier, EOperation, |
| EStructuralFeature, EEnumLiteral, EParameter, EObject, |
| CallOperationAction, SendSignalAction, Constraint, EClass, EObject> |
| factory) { |
| super.setFactory(factory); |
| } |
| |
| // use the AbstractEnvironment's mechanism for defining |
| // "additional operations" to add our custom operation to |
| // OCL's String primitive type |
| private void defineCustomOperations() { |
| // pattern-matching operation |
| regexMatch = EcoreFactory.eINSTANCE.createEOperation(); |
| regexMatch.setName("regexMatch"); |
| regexMatch.setEType(getOCLStandardLibrary().getString()); |
| EParameter parm = EcoreFactory.eINSTANCE.createEParameter(); |
| parm.setName("pattern"); |
| parm.setEType(getOCLStandardLibrary().getString()); |
| regexMatch.getEParameters().add(parm); |
| |
| // annotate it so that we will recognize it |
| // in the evaluation environment |
| EAnnotation annotation = EcoreFactory.eINSTANCE.createEAnnotation(); |
| annotation.setSource("MyEnvironment"); |
| regexMatch.getEAnnotations().add(annotation); |
| |
| // define it as an additional operation on OCL String |
| addOperation(getOCLStandardLibrary().getString(), regexMatch); |
| } |
| } |
| p. |
| |
| Next, we will define the corresponding specialization of the |
| "@EcoreEvaluationEnvironment@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/EcoreEvaluationEnvironment.html that will know how to evaluate calls to this custom operation: |
| |
| bc.. |
| class MyEvaluationEnvironment extends EcoreEvaluationEnvironment { |
| MyEvaluationEnvironment() { |
| super(); |
| } |
| |
| MyEvaluationEnvironment( |
| EvaluationEnvironment<EClassifier, EOperation, EStructuralFeature, |
| EClass, EObject> parent) { |
| super(parent); |
| } |
| |
| public Object callOperation(EOperation operation, int opcode, |
| Object source, Object[] args) { |
| if (operation.getEAnnotation("MyEnvironment") == null) { |
| // not our custom regex operation |
| return super.callOperation(operation, opcode, source, args); |
| } |
| |
| if ("regexMatch".equals(operation.getName())) { |
| Pattern pattern = Pattern.compile((String) args[0]); |
| Matcher matcher = pattern.matcher((String) source); |
| |
| return matcher.matches()? matcher.group() : null; |
| } |
| |
| throw new UnsupportedOperationException(); // unknown operation |
| } |
| } |
| p. |
| |
| Finally, we define a specialization of the |
| "@EcoreEnvironmentFactory@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/EcoreEnvironmentFactory.html that creates our custom environments: |
| |
| bc.. |
| class MyEnvironmentFactory extends EcoreEnvironmentFactory { |
| public Environment<EPackage, EClassifier, EOperation, EStructuralFeature, |
| EEnumLiteral, EParameter, EObject, CallOperationAction, |
| SendSignalAction, Constraint, EClass, EObject> createEnvironment() { |
| MyEnvironment result = new MyEnvironment(getEPackageRegistry()); |
| result.setFactory(this); |
| return result; |
| } |
| |
| public Environment<EPackage, EClassifier, EOperation, EStructuralFeature, |
| EEnumLiteral, EParameter, EObject, CallOperationAction, |
| SendSignalAction, Constraint, EClass, EObject> |
| createEnvironment(Environment<EPackage, EClassifier, EOperation, |
| EStructuralFeature, EEnumLiteral, EParameter, EObject, |
| CallOperationAction, SendSignalAction, Constraint, EClass, |
| EObject> parent) { |
| if (!(parent instanceof MyEnvironment)) { |
| throw new IllegalArgumentException( |
| "Parent environment must be my environment: " + parent); |
| } |
| |
| MyEnvironment result = new MyEnvironment((MyEnvironment) parent); |
| result.setFactory(this); |
| return result; |
| } |
| |
| public EvaluationEnvironment<EClassifier, EOperation, EStructuralFeature, |
| EClass, EObject> createEvaluationEnvironment() { |
| return new MyEvaluationEnvironment(); |
| } |
| |
| public EvaluationEnvironment<EClassifier, EOperation, EStructuralFeature, |
| EClass, EObject> createEvaluationEnvironment( |
| EvaluationEnvironment<EClassifier, EOperation, EStructuralFeature, |
| EClass, EObject> parent) { |
| return new MyEvaluationEnvironment(parent); |
| } |
| } |
| p. |
| |
| Now, we can use our environment to parse the kind of expression that we were looking for: |
| |
| bc.. |
| OCL<?, EClassifier, ?, ?, ?, ?, ?, ?, ?, Constraint, EClass, EObject> ocl; |
| ocl = OCL.newInstance(new MyEnvironmentFactory()); |
| |
| OCLHelper<EClassifier, ?, ?, Constraint> helper = ocl.createOCLHelper(); |
| helper.setContext(MyPackage.Literals.PERSON); |
| |
| // double the '\' to escape it in a Java string literal |
| Constraint validSSN = helper.createInvariant( |
| "self.ssn.regexMatch('\\d{3}-\\d{3}-\\d{3}') <> null"); |
| |
| Person person = getPersonToValidate(); |
| |
| System.out.printf("%s valid SSN: %b%n", person, ocl.check(person, validSSN)); |
| p. |
| |
| h3. Selecting a Package Lookup Strategy |
| |
| When package names are provided in OCL expressions, e.g., when representing types in an |
| @oclIsKindOf@ call, these names are looked up using a specific |
| strategy. By default, the lookup proceeds starting at the parsing context, traversing |
| up the package hierarchy. If the package name cannot be resolved this way, for the Ecore |
| binding a lookup is performed in the @EPackage.Registry@. By |
| default, the package name provided is compared to the names of the packages that are |
| contained as values in the registry. |
| |
| In rare cases there may be ambiguous package names. For example, if an OCL expression |
| is to be parsed using a classifier from the OCL AST metamodel as its context, the |
| context package is @ocl::ecore@. If such an expression is |
| trying to reference a type from the EMF Ecore package with package name |
| @ecore@, the EMF Ecore package is hidden by the lookup |
| happening relative to the context package. Instead of the EMF Ecore package, the |
| @ocl::ecore@ package will be found. |
| |
| Such an ambiguity can be resolved by using a dedicated @EPackage.Registry@ |
| which registers the otherwise ambiguous packages with a special "URI" that represents a |
| simple alias name for the package. In order to force the OCL parser to look up packages |
| by those alias names, an option needs to be set on the OCL environment, as follows: |
| |
| bc.. |
| Registry r = new EPackageRegistryImpl(); |
| r.putAll(EPackage.Registry.INSTANCE); |
| r.put("EMFEcore", EcorePackage.eINSTANCE); |
| r.put("OCLEcore", org.eclipse.ocl.ecore.EcorePackage.eINSTANCE); |
| OCL ocl = OCL.newInstance(new EcoreEnvironmentFactory(r)); |
| ((EcoreEnvironment) ocl.getEnvironment()).setOption( |
| ParsingOptions.PACKAGE_LOOKUP_STRATEGY, |
| ParsingOptions.PACKAGE_LOOKUP_STRATEGIES. |
| LOOKUP_PACKAGE_BY_ALIAS_THEN_NAME); |
| Helper helper = ocl.createOCLHelper(); |
| helper.setContext( |
| org.eclipse.ocl.ecore.EcorePackage.eINSTANCE.getOCLExpression()); |
| org.eclipse.ocl.ecore.OCLExpression expr = helper.createQuery( |
| "self.oclIsKindOf(EMFEcore::EClassifier) and not |
| self.oclIsKindOf(OCLEcore::OCLExpression)"); |
| p. |
| |
| In the example above, two packages with ambiguous simple names (EMF Ecore package and |
| OCL Ecore package, both with simple name @ecore@) are added with |
| alias names @EMFEcore@ and @OCLEcore@, |
| respectively. The package lookup strategy is then set to |
| @LOOKUP_PACKAGE_BY_ALIAS_THEN_NAME@ which allows OCL expressions to reference |
| the packages by their aliases, as in |
| @self.oclIsKindOf(EMFEcore::EClassifier) and not self.oclIsKindOf(OCLEcore::OCLExpression)@. |
| |
| Note, that the use of a delegating registry (constructor |
| @EPackageRegistryImpl(EPackage.Registry)@) does not work |
| because a registry initialized this way does not forward the call to |
| @values()@ which would be required by the OCL |
| package lookup implementation. Instead, if the packages registered with the |
| default registry are required, they need to be copied to a new registry |
| using @putAll@ as shown above. |
| |
| h3. Customizing Hidden Opposite Lookup and Navigation |
| |
| The default "@EcoreEnvironmentFactory@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/EcoreEnvironmentFactory.html produces environments which can find references that have an annotation with source @http://schema.omg.org/spec/MOF/2.0/emof.xml@ |
| that have a detail with key @Property.oppositeRoleName@. In the class that is the type of the reference, |
| and all its subclasses, for OCL this annotation defines an otherwise "hidden" opposite property which can be used |
| in OCL expressions. This can be convenient when it is not possible or desirable to define an explicit |
| opposite reference, e.g., because the class that would have to own the opposite reference can't easily be |
| modified or the serialization of that class must not be changed. |
| |
| The logic used to find these "hidden" opposites and to navigate them is provided by implementations |
| of the "@OppositeEndFinder@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/OppositeEndFinder.html interface. By default, the @EcoreEnvironmentFactory@ uses the |
| "@DefaultOppositeEndFinder@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/DefaultOppositeEndFinder.html implementation. It performs the lookup of annotated references by maintaining a cache based on |
| the Ecore package registry. Successful navigation of those "hidden" opposites requires an |
| "@ECrossReferenceAdapter@":http://download.eclipse.org/modeling/emf/emf/javadoc/2.6.0/org/eclipse/emf/ecore/util/ECrossReferenceAdapter.html to be registered for the containment hierarchy or the resource or resource set that should be used as the scope of the navigation. |
| |
| Obviously, "@ECrossReferenceAdapter@":http://download.eclipse.org/modeling/emf/emf/javadoc/2.6.0/org/eclipse/emf/ecore/util/ECrossReferenceAdapter.html has a significant downside: it responds to "hidden" opposite navigation requests only based on what has so far been loaded by EMF. If the set of resources held by an underlying EMF storage system contains more resources than have so far been loaded into the resource set, non-loaded content from that storage system won't be considered by the @ECrossReferenceAdapter@. Given a store with reasonable search capabilities it is desirable to take advantage of these capabilities also to perform reverse navigation of those "hidden" opposites. To achieve this, a specific implementation of the |
| "@OppositeEndFinder@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/OppositeEndFinder.html interface can be provided. It may be a specialization of |
| "@DefaultOppositeEndFinder@":http://download.eclipse.org/ocl/javadoc/6.4.0/org/eclipse/ocl/ecore/DefaultOppositeEndFinder.html, e.g., when the reference lookup based on the Ecore package registry is sufficient and only the navigation behavior shall be redefined: |
| |
| bc.. |
| class MyOppositeEndFinder extends DefaultOppositeEndFinder { |
| MyOppositeEndFinder(EPackage.Registry registry) { |
| super(registry); |
| } |
| |
| @Override |
| public Object navigateOppositeProperty(EStructuralFeature property, Object target) { |
| Collection<Object> result = null; |
| EObject eTarget = (EObject) target; |
| // do something clever, e.g., using your underlying store's query facility or |
| // the new EMF Query2 component (incubation) |
| // ... |
| return result; |
| } |
| } |
| p. |
| |
| With this, OCL can be instantiated using the custom opposite end finder as follows: |
| |
| bc.. |
| OCL ocl = OCL.newInstance(new EcoreEnvironmentFactoryWithHiddenOpposites( |
| EPackage.Registry.INSTANCE, new MyOppositeEndFinder())); |
| ... |
| p. |
| |
| With this, when the use of a property in an OCL expression cannot be resolved to an attribute |
| or reference, the opposite end finder is asked to look for a correspondingly-named "hidden" |
| opposite. Navigation across this "hidden" opposite will then call the @navigateOppositeProperty@ method on @MyOppositeEndFinder@. |
| |