OCL Engine User Guide
Fennec OCL is a lightweight, spec-compliant OCL v2.5 engine (backward compatible with v2.4) that works as a standalone Java library — no Eclipse platform required.
Table of Contents
- Overview
- Quick Start
- Engine Setup
- Evaluating Expressions
- OclContext — Evaluation Context
- Evaluation Options
- Caching
- Error Handling
- EMF Delegate Integration
- Complete OCL Documents
- Custom Operations
- Thread Safety
- Value Type Mapping
- Security Hardening
1. Overview
The Fennec OCL Engine provides:
- OCL v2.5 expression parsing and evaluation (backward compatible with v2.4)
- Standalone operation — works as a plain Java library without OSGi
- OSGi-optional — full Declarative Services support when running in OSGi
- ANTLR4-based parser — fast, reliable parsing with precise error locations
- EMF delegate integration — derived features, operation bodies, and validation constraints
- LRU expression cache — thread-safe caching with hit/miss statistics
- Configurable evaluation — strict/lenient null handling, timeouts, depth limits
Performance
Fennec OCL is significantly faster than Eclipse OCL Classic:
| Operation | Speedup |
|---|---|
| Parse (no cache) | 100–315x faster |
| Parse (cached) | ~100,000x faster |
| Parse + Eval | 176–454x faster |
| Pure Evaluation | 20–59% faster |
See benchmark-results.md for detailed numbers.
Maven Coordinates
GroupId: org.eclipse.fennec.m2x| Bundle | Description |
|---|---|
org.eclipse.fennec.m2x.ocl.api | Public API interfaces |
org.eclipse.fennec.m2x.ocl.parser | ANTLR4 parser |
org.eclipse.fennec.m2x.ocl.engine | Evaluator implementation |
org.eclipse.fennec.m2x.ocl.model | OCL EMF metamodel |
2. Quick Start
Minimal example — parse and evaluate an OCL expression against an EMF object:
import org.eclipse.fennec.m2x.ocl.api.OclContext;
import org.eclipse.fennec.m2x.ocl.api.OclEngine;
import org.eclipse.fennec.m2x.ocl.engine.OclEngineImpl;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
// Create engine
OclEngine engine = new OclEngineImpl(new OclParserSupport());
// Evaluate an expression
Object result = engine.evaluate("self.name.size() > 0", OclContext.of(myEObject));
System.out.println(result); // trueThat's it. Three lines to set up, one line to evaluate.
3. Engine Setup
3.1 Minimal (No Cache)
import org.eclipse.fennec.m2x.ocl.engine.OclEngineImpl;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
OclEngine engine = new OclEngineImpl(new OclParserSupport());3.2 With LRU Expression Cache
import org.eclipse.fennec.m2x.ocl.engine.OclEngineImpl;
import org.eclipse.fennec.m2x.ocl.engine.OclLruExpressionCache;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
OclEngine engine = new OclEngineImpl(
new OclParserSupport(),
OclLruExpressionCache.ofSize(2048)
);3.3 With OclConfiguration Builder (Recommended)
import org.eclipse.fennec.m2x.ocl.api.OclConfiguration;
import org.eclipse.fennec.m2x.ocl.engine.OclEngineImpl;
import org.eclipse.fennec.m2x.ocl.engine.OclLruExpressionCache;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
OclConfiguration config = OclConfiguration.builder(new OclParserSupport())
.expressionCache(OclLruExpressionCache.ofSize(2048))
.addOperationProvider(myCustomOps)
.build();
OclEngine engine = new OclEngineImpl(config);3.4 OSGi (Declarative Services)
In OSGi, the engine and parser are registered as PROTOTYPE-scoped DS components. Every @Reference injection creates a fresh, isolated instance:
import org.eclipse.fennec.m2x.ocl.api.OclEngine;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@Component
public class MyComponent {
@Reference
private OclEngine engine; // fresh instance, own PropertyAccessorCache
public boolean validateName(EObject obj) {
return (Boolean) engine.evaluate("self.name.size() > 0", OclContext.of(obj));
}
}Component scopes:
| Component | Scope | What's shared? |
|---|---|---|
OclParserSupport | PROTOTYPE | Nothing — each engine gets its own parser |
OclEngineComponent | PROTOTYPE | Nothing — each consumer gets its own engine with isolated PropertyAccessorCache |
DefaultOclExpressionCacheComponent | SINGLETON | Shared by default — all engines share the same LRU parse cache (1024 entries) |
The expression cache is shared because parsed OclExpression ASTs are immutable and thread-safe. This means a parse result from one engine benefits all others.
To use individual caches (e.g. for different metamodels), create factory configurations with expressionCache.target — see §3.5 and the Architecture Doc §9.5.
3.5 OSGi Configuration via ConfigAdmin
The OclEngineComponent supports engine-wide defaults through OSGi ConfigurationAdmin. All properties use the ocl. prefix.
OSGi Configurator JSON:
{
":configurator:resource-version": 1,
"DefaultOclEngine": {
"ocl.maxDepth": 500,
"ocl.maxCollectionSize": 100000,
"ocl.maxClosureIterations": 10000,
"ocl.maxRegexLength": 200,
"ocl.timeout": 5000,
"ocl.nullHandling": "STRICT",
"ocl.errorRecovery": "FAIL_FAST",
"ocl.customOperationsEnabled": false
}
}Available properties:
| Property | Type | Default | Description |
|---|---|---|---|
ocl.maxDepth | int | 1,000 | Maximum expression nesting depth |
ocl.maxCollectionSize | int | 1,000,000 | Maximum collection elements |
ocl.maxClosureIterations | int | 100,000 | Maximum closure() iterations |
ocl.maxRegexLength | int | 1,000 | Maximum regex pattern length |
ocl.timeout | long | 0 | Evaluation timeout in ms (0 = no timeout) |
ocl.nullHandling | String | STRICT | STRICT or LENIENT |
ocl.errorRecovery | String | FAIL_FAST | FAIL_FAST or COLLECT_ERRORS |
ocl.customOperationsEnabled | boolean | false | Enable config-registered custom operations |
These defaults are used by evaluate(expression, context) (without explicit options). Calls with explicit OclEvaluationOptions override the engine-wide defaults.
Multiple engine configurations:
Use factory configurations to create multiple engines with different settings:
{
":configurator:resource-version": 1,
"DefaultOclEngine~strict": {
"ocl.maxDepth": 100,
"ocl.nullHandling": "STRICT",
"engine.tag": "strict"
},
"DefaultOclEngine~lenient": {
"ocl.maxDepth": 1000,
"ocl.nullHandling": "LENIENT",
"engine.tag": "lenient"
}
}Inject a specific engine via filter:
@Reference(target = "(engine.tag=strict)")
private OclEngine strictEngine;
@Reference(target = "(engine.tag=lenient)")
private OclEngine lenientEngine;3.6 Standalone Configuration with Engine-Wide Defaults
The OclConfiguration builder also supports engine-wide defaults for standalone (non-OSGi) use:
OclConfiguration config = OclConfiguration.builder(new OclParserSupport())
.expressionCache(OclLruExpressionCache.ofSize(2048))
.nullHandling(NullHandling.LENIENT)
.errorRecovery(ErrorRecovery.COLLECT_ERRORS)
.maxDepth(500)
.timeoutMs(5000)
.maxCollectionSize(100_000)
.maxClosureIterations(10_000)
.maxRegexLength(200)
.build();
OclEngine engine = new OclEngineImpl(config);
// evaluate() without options uses the engine-wide defaults
Object result = engine.evaluate("self.name", OclContext.of(obj));
// evaluate() with explicit options overrides the defaults
Object sandboxed = engine.evaluate(expr, ctx,
OclEvaluationOptions.strict().withMaxDepth(50));4. Evaluating Expressions
4.1 Convenience: Parse + Eval in One Step
The simplest approach — pass a String expression and context:
Object result = engine.evaluate("self.name", OclContext.of(myEObject));This parses and evaluates in one call. If a cache is configured, the parsed expression is cached automatically.
4.2 Pre-Parsed: Parse Once, Evaluate Many Times
For repeated evaluation of the same expression against different objects:
import org.eclipse.fennec.m2x.model.ocl.OclExpression;
// Parse once
OclExpression expr = engine.parse("self.name.size() > 3", myEClass);
// Evaluate many times
for (EObject obj : objects) {
Object result = engine.evaluate(expr, OclContext.of(obj));
}4.3 With Diagnostics: Fault-Tolerant Evaluation
Use evaluateWithDiagnostics() to collect warnings and errors without throwing:
import org.eclipse.fennec.m2x.ocl.api.OclEvaluationOptions;
import org.eclipse.fennec.m2x.ocl.api.OclResult;
OclResult result = engine.evaluateWithDiagnostics(
expr,
OclContext.of(myEObject),
OclEvaluationOptions.lenient()
);
if (result.isSuccess()) {
String name = result.getValueAs(String.class);
} else {
result.diagnostics().forEach(d -> System.err.println(d.getMessage()));
}4.4 With Custom Evaluation Options
import org.eclipse.fennec.m2x.ocl.api.OclEvaluationOptions;
Object result = engine.evaluate(
expr,
OclContext.of(myEObject),
OclEvaluationOptions.strict().withTimeout(Duration.ofSeconds(5))
);5. OclContext — Evaluation Context
OclContext is a Java record that defines the evaluation environment.
5.1 Factory Methods
// Just a context object (self)
OclContext ctx = OclContext.of(myEObject);
// With external variables
OclContext ctx = OclContext.of(myEObject, Map.of("threshold", 42));
// With model extent (enables allInstances())
OclContext ctx = OclContext.of(myEObject, myExtent);
// With ResourceSet (for package resolution)
OclContext ctx = OclContext.of(myEObject, myResourceSet);
// With extent and ResourceSet
OclContext ctx = OclContext.of(myEObject, myExtent, myResourceSet);
// Variables only (no self)
OclContext ctx = OclContext.of(Map.of("x", 1, "y", 2));5.2 Full Constructor
OclContext ctx = new OclContext(
myEObject, // self — context object (nullable)
myExtent, // OclModelExtent — scope for allInstances() (nullable)
Map.of("x", 42), // variables — external variable bindings
myResourceSet, // ResourceSet — for EPackage resolution (nullable)
myInterceptor // BiFunction<EObject, String, Object> — property interceptor (nullable)
);5.3 OclModelExtent — allInstances() Scope
To use allInstances() in OCL expressions, provide an OclModelExtent:
import org.eclipse.fennec.m2x.ocl.api.OclModelExtent;
OclModelExtent extent = eClass -> {
// Return all instances of the given EClass in your model
return myResource.getAllContents()
.filter(eClass::isInstance)
.toList();
};5.4 Property Interceptor
Intercept property access for custom resolution (e.g., aspect-oriented properties):
BiFunction<EObject, String, Object> interceptor = (obj, propertyName) -> {
if ("customProp".equals(propertyName)) {
return computeCustomValue(obj);
}
return OclContext.PROPERTY_NOT_HANDLED; // fall back to default
};
OclContext ctx = new OclContext(myEObject, null, Map.of(), null, interceptor);6. Evaluation Options
OclEvaluationOptions controls how the engine handles nulls, errors, and resource limits.
There are three levels of configuration, from broadest to narrowest:
- Engine-wide defaults — set via
OclConfigurationbuilder (standalone) or ConfigAdmin (OSGi). Used byevaluate(expression, context). - Delegate defaults — set via
setDelegateOptions(). Used by EMF delegate evaluations. - Per-evaluation options — passed explicitly to
evaluate(expression, context, options). Override engine-wide defaults.
6.1 Presets
// Strict: null access → OclInvalid, stop on first error
OclEvaluationOptions strict = OclEvaluationOptions.strict();
// Lenient: null access → null, collect all errors
OclEvaluationOptions lenient = OclEvaluationOptions.lenient();6.2 Custom Options
All with* methods return new immutable instances:
OclEvaluationOptions options = OclEvaluationOptions.strict()
.withTimeout(Duration.ofSeconds(10))
.withMaxDepth(500)
.withMaxCollectionSize(100_000)
.withMaxClosureIterations(50_000)
.withMaxRegexLength(500)
.withUseEMFTypes(true);6.3 Option Reference
| Option | Default | Description |
|---|---|---|
NullHandling.STRICT | yes | Null property access returns OclInvalid.INSTANCE |
NullHandling.LENIENT | Null property access returns null | |
ErrorRecovery.FAIL_FAST | yes | Stop on first error |
ErrorRecovery.COLLECT_ERRORS | Collect all errors, continue evaluation | |
maxDepth | 1,000 | Maximum expression nesting depth |
timeout | none | Maximum evaluation time |
maxCollectionSize | 1,000,000 | Maximum elements in a collection |
maxClosureIterations | 100,000 | Maximum closure() iterations |
maxRegexLength | 1,000 | Maximum regex pattern length |
useEMFTypes | false | Wrap top-level Collection as EList, top-level Map as EMap |
6.4 Return Types — Java vs. EMF
By default, OclEngine.evaluate(...) returns OCL-spec-native Java types:
| OCL type | Java class (default) |
|---|---|
Sequence(T) | java.util.ArrayList |
OrderedSet(T) | OclOrderedSet (extends ArrayList, OCL equality) |
Bag(T) | OclBag (extends ArrayList) |
Set(T) | OclSet (extends AbstractSet, OCL equality) |
Map(K,V) | java.util.LinkedHashMap |
Integer | Integer (narrowed from Long when it fits) |
When a caller expects EMF-shaped results — e.g. the result is being assigned to an EList<T>-typed variable or fed back into EMF APIs — set useEMFTypes = true. The engine then wraps top-level only collection/map results:
Collection(any OCL kind) → unmodifiableEListMap→ unmodifiableEMap
Nested collections inside the result are left untouched. Non-collection values (numbers, strings, EObjects, …) pass through unchanged.
Example
OclEvaluationOptions options = OclEvaluationOptions.strict()
.withUseEMFTypes(true);
// "self.employees->select(e | e.name.startsWith(prefix))->asSequence()"
EList<Person> matches = (EList<Person>) engine.evaluate(parsed, ctx, options);The same flag can be set once on the engine configuration so every call uses it:
OclConfiguration config = OclConfiguration.builder(parser)
.useEMFTypes(true)
.build();
OclEngine engine = new OclEngineImpl(config);Delegate note: The EMF invocation delegate always returns an
EListfor multi-valuedEOperations regardless of this flag — that is required by the EMF contract (see issue #3). TheuseEMFTypesflag (see issue #4) only affects the shape ofOclEngine.evaluate(...)return values.
7. Caching
7.1 Expression Cache
The OclLruExpressionCache caches parsed OclExpression ASTs keyed by (expression, contextType):
import org.eclipse.fennec.m2x.ocl.engine.OclLruExpressionCache;
OclLruExpressionCache cache = OclLruExpressionCache.ofSize(2048);
OclEngine engine = new OclEngineImpl(new OclParserSupport(), cache);
// Cache statistics
long hits = cache.hitCount();
long misses = cache.missCount();
long size = cache.size();
double hitRate = (double) hits / (hits + misses);- Thread-safe — internally synchronized
- LRU eviction — least-recently-used entries evicted when full
- Cache key —
nsURI#contextTypeName#expression
7.2 WarmUp
Pre-populate the PropertyAccessorCache and parse common expressions:
OclEngineImpl engine = new OclEngineImpl(config);
engine.warmUp(MyPackage.eINSTANCE);This caches property accessors for all classes in the package, reducing first-evaluation latency.
7.3 Performance Impact
From benchmarks with cache enabled:
| Scenario | Without Cache | With Cache | Speedup |
|---|---|---|---|
| Simple expression (repeated) | 6.66 ms | ~0.06 ms | ~100x |
| Medium expression (repeated) | 34.52 ms | ~0.35 ms | ~100x |
8. Error Handling
8.1 Parse Errors
OclParseException is a checked exception with line/column information:
try {
engine.parse("self.name +++ 42", myEClass);
} catch (OclParseException e) {
System.err.println(e.getMessage());
for (Resource.Diagnostic error : e.getErrors()) {
System.err.printf(" Line %d, Col %d: %s%n",
error.getLine(), error.getColumn(), error.getMessage());
}
}8.2 OclInvalid vs. null
OCL distinguishes three value states:
| State | Meaning | Check |
|---|---|---|
| Regular value | Normal result | result != null && !(result instanceof OclInvalid) |
null | OCL void/undefined | result == null |
OclInvalid.INSTANCE | OCL invalid (error) | result instanceof OclInvalid |
8.3 OclResult — Three States
When using evaluateWithDiagnostics():
OclResult result = engine.evaluateWithDiagnostics(expr, ctx, options);
if (result.hasValue()) {
// Normal value — no errors, value is present and not invalid
process(result.getValueAs(String.class));
} else if (result.isInvalid()) {
// OclInvalid — the expression evaluated to invalid
handleInvalid(result.diagnostics());
} else if (result.isNull()) {
// null — the expression evaluated to void/undefined
handleNull();
}
// Or simply check for success (no ERROR-level diagnostics)
if (result.isSuccess()) {
// safe to use result.value()
}9. EMF Delegate Integration
Register the OCL engine as an EMF delegate for derived features, operation bodies, and validation constraints.
9.1 Standalone Registration
OclEngineImpl engine = new OclEngineImpl(config);
engine.installDelegates();
// Now EMF will use Fennec OCL for:
// - EOperation invocation delegates (body annotations)
// - EStructuralFeature setting delegates (derivation/initial annotations)
// - EValidator validation delegates (constraint annotations)
// Cleanup when done
engine.uninstallDelegates();9.2 Delegate URI
The native Fennec OCL delegate URI is:
http://www.eclipse.org/fennec/m2x/ocl/1.0The engine also serves the legacy Eclipse OCL Pivot delegate URI for interop (see 9.5):
http://www.eclipse.org/emf/2002/Ecore/OCL/PivotBoth are exposed as OclDelegateUtil.DELEGATE_URI and OclDelegateUtil.LEGACY_PIVOT_URI; the full set served by the engine is OclDelegateUtil.SERVED_URIS.
9.3 Ecore Annotations
In your .ecore model, add annotations with this delegate URI:
Derived Feature:
<eStructuralFeatures xsi:type="ecore:EAttribute" name="fullName" eType="ecore:EDataType ...">
<eAnnotations source="http://www.eclipse.org/fennec/m2x/ocl/1.0">
<details key="derivation" value="self.firstName + ' ' + self.lastName"/>
</eAnnotations>
</eStructuralFeatures>Operation Body:
<eOperations name="isAdult" eType="ecore:EDataType ...">
<eAnnotations source="http://www.eclipse.org/fennec/m2x/ocl/1.0">
<details key="body" value="self.age >= 18"/>
</eAnnotations>
</eOperations>Validation Constraint:
<eAnnotations source="http://www.eclipse.org/fennec/m2x/ocl/1.0">
<details key="nameNotEmpty" value="self.name.size() > 0"/>
</eAnnotations>9.4 OSGi
In OSGi, the delegate factories (OclInvocationDelegateFactory, OclSettingDelegateFactory, OclValidationDelegateFactory) are registered automatically as DS components with emf.osgi whiteboard properties. The emf.osgi delegate registry components discover them and populate the global EMF registries. No manual installDelegates() call needed.
Each delegate factory injects OclEngineImpl via @Reference to access both the public API methods (parse, evaluate) and internal delegate methods (getDelegateOptions, evaluatePostcondition).
9.5 Legacy Eclipse OCL Pivot Namespace
The Fennec engine also serves the legacy Eclipse OCL Pivot delegate URI:
http://www.eclipse.org/emf/2002/Ecore/OCL/PivotThis lets models authored against Eclipse OCL — whose derived features, operation bodies, and constraints are annotated under the Pivot URI rather than the Fennec URI — evaluate with the Fennec engine without re-annotating the model. The Fennec OCL dialect is a superset of Eclipse OCL's, so no expression translation is involved; only the annotation source differs.
Both URIs are registered on every path:
- Standalone —
installDelegates()registers each factory under all served URIs;uninstallDelegates()removes them all. - OSGi — the emf.osgi delegate registry reads
emf.configuratorNameas a single value, so each delegate type ships a thin companion component for the legacy URI (OclLegacyPivotSettingDelegateFactory,OclLegacyPivotInvocationDelegateFactory,OclLegacyPivotValidationDelegateFactory). These subclass the Fennec factories and only differ in their whiteboard properties (emf.configuratorName= the Pivot URI,emf.name=fennec-ocl-pivot).
The factories resolve the OCL expression by consulting the served URIs in order (Fennec first, Pivot as fallback), so a feature annotated under either URI is picked up transparently. An example of a model that relies on this is org.eclipse.daanse.cwm.model.cwmx.eorm, whose derived features (e.g. Column.name = cwmColumn.name) are annotated under the legacy Pivot URI.
To serve additional legacy or third-party delegate URIs, add them to OclDelegateUtil.SERVED_URIS — standalone registration, expression lookup, and warm-up all iterate that list. For OSGi, also add a companion component for the new URI (one per delegate type), mirroring the OclLegacyPivot* factories.
Note: This changes only the set of annotation sources the engine answers to. The OCL expressions themselves are handled exactly as before.
10. Complete OCL Documents
Load standalone .ocl documents that define additional operations, properties, and constraints for existing Ecore models.
10.1 Parse a Document
import org.eclipse.fennec.m2x.model.ocl.Constraint;
List<Constraint> constraints = engine.parseDocument(
"""
context Person
inv nameNotEmpty: self.name.size() > 0
inv agePositive: self.age >= 0
context Person
def: fullName : String = self.firstName + ' ' + self.lastName
"""
);10.2 Load and Register
loadDocument() parses the document and registers all definitions (def:, inv:) with the engine:
engine.loadDocument(oclDocumentText);
// Now def:-operations are available in subsequent evaluations
Object fullName = engine.evaluate("self.fullName", OclContext.of(personObject));10.3 With ResourceSet
If the document references types from specific packages:
List<Constraint> constraints = engine.parseDocument(oclDocument, myResourceSet);10.4 OSGi: CompleteOclContribution
In OSGi, deploy Complete OCL documents as whiteboard services:
import org.eclipse.fennec.m2x.ocl.api.CompleteOclContribution;
@Component(service = CompleteOclContribution.class)
public class MyOclConstraints implements CompleteOclContribution {
@Override
public URI getDocumentUri() {
return URI.createURI("platform:/plugin/my.bundle/model/constraints.ocl");
}
@Override
public String getDocumentText() {
return """
context Person
inv nameNotEmpty: self.name.size() > 0
""";
}
}11. Custom Operations
Extend the OCL standard library with custom operations.
11.1 Define an Operation
Use OclOperation.of() for simple no-arg operations or the full constructor for operations with parameters:
import org.eclipse.fennec.m2x.ocl.api.OclOperation;
// Simple no-arg operation using factory method
OclOperation toUpperCase = OclOperation.of(
"toUpperCase", // operation name
stringOclType, // owner type (OclType instance for String)
stringOclType, // return type
(self, args) -> ((String) self).toUpperCase()
);11.2 Register via Provider
public class MyOperations implements OclOperationProvider {
@Override
public List<OclOperation> getOperations() {
return List.of(
new OclOperation(
"trimToNull",
stringType,
List.of(), // no parameters
stringType,
(self, args) -> {
String s = ((String) self).trim();
return s.isEmpty() ? null : s;
}
)
);
}
}
// Standalone registration via configuration (D29: no runtime registration)
OclConfiguration config = OclConfiguration.builder(new OclParserSupport())
.operationProviders(List.of(new MyOperations()))
.customOperationsEnabled(true) // D29: required opt-in
.build();
OclEngine engine = new OclEngineImpl(config);11.3 OSGi Registration
In OSGi, the OclEngineComponent injects exactly one OclOperationProvider via a mandatory @Reference. By default, the built-in NoOpOclOperationProvider is bound (returns an empty list — no custom operations).
To provide custom operations, register your provider as a DS component and configure the engine to select it via operationProvider.target:
@Component(service = OclOperationProvider.class,
property = "provider.name=myProvider")
public class MyOperations implements OclOperationProvider {
@Override
public List<OclOperation> getOperations() {
return List.of(/* ... */);
}
}Then configure OclEngineComponent via ConfigAdmin or OSGi Configurator:
{
"DefaultOclEngine": {
"operationProvider.target": "(provider.name=myProvider)",
"ocl.customOperationsEnabled": true
}
}Both operationProvider.target and customOperationsEnabled are required — the target selects the provider, and the D29 gate enables operation resolution at evaluation time (see §14 Security).
If you need multiple providers combined, create a compound provider that aggregates them and register it as a single service.
11.4 Using OclConfiguration
OclConfiguration config = OclConfiguration.builder(parser)
.addOperationProvider(new MyOperations())
.addOperationProvider(new MoreOperations())
.build();12. Thread Safety
| Component | Thread-Safe? | Notes |
|---|---|---|
OclEngineImpl | Yes | Stateless evaluation; shared parser and cache are synchronized |
OclLruExpressionCache | Yes | Internally synchronized with atomic counters |
OclContext | Yes | Immutable record |
OclEvaluationOptions | Yes | Immutable record |
OclResult | Yes | Immutable record |
OclExpression (parsed AST) | Yes | Immutable after parsing; safe to share across threads |
OclParserSupport | Yes | Thread-safe parser implementation |
installDelegates() | No | Modifies global EMF registries — call once at startup |
Recommended pattern for multi-threaded use:
// Create once, share across threads
OclEngine engine = new OclEngineImpl(config);
// Each thread creates its own context
ExecutorService pool = Executors.newFixedThreadPool(8);
for (EObject obj : objects) {
pool.submit(() -> {
OclContext ctx = OclContext.of(obj);
return engine.evaluate("self.name", ctx);
});
}13. Value Type Mapping
See also Section 6.4 for the exact runtime classes used for OCL collections and the
useEMFTypesoption that switches top-levelCollection/Mapreturns toEList/EMap.
OCL types map to Java types as follows:
| OCL Type | Java Type | Notes |
|---|---|---|
Boolean | java.lang.Boolean | |
Integer | java.lang.Integer | Promoted to Long for large values |
Real | java.lang.Double | |
String | java.lang.String | |
UnlimitedNatural | java.lang.Integer | * maps to -1 |
OclVoid | null | |
OclInvalid | OclInvalid.INSTANCE | Singleton |
Set(T) | java.util.Set<T> | LinkedHashSet (preserves insertion order) |
OrderedSet(T) | java.util.Set<T> | LinkedHashSet |
Bag(T) | java.util.List<T> | ArrayList (duplicates allowed) |
Sequence(T) | java.util.List<T> | ArrayList |
Tuple | java.util.Map<String, Object> | Keys are part names |
| Any EClass | org.eclipse.emf.ecore.EObject | |
| Any EEnum | EMF-generated enum literal | |
| Any EDataType | Corresponding Java instance class |
14. Security Hardening
When evaluating OCL expressions from untrusted sources (user input, external Complete OCL documents, model annotations from unknown models), configure conservative resource limits to prevent denial-of-service attacks.
Configurable Limits
| Field | Type | Default | Protects against |
|---|---|---|---|
maxDepth | int | 1,000 | Stack overflow via deeply nested expressions |
maxCollectionSize | int | 1,000,000 | Range explosion, product explosion, allInstances |
maxClosureIterations | int | 100,000 | Unbounded closure traversal |
maxRegexLength | int | 1,000 | ReDoS via crafted regex patterns |
timeout | Duration | none | Runaway evaluation (deadline-based enforcement) |
All limits produce OclInvalid with a diagnostic error when exceeded.
Sandboxed Evaluation
import java.time.Duration;
// Configure conservative limits for untrusted input
OclEvaluationOptions sandboxed = OclEvaluationOptions.strict()
.withMaxDepth(100)
.withMaxCollectionSize(10_000)
.withMaxClosureIterations(1_000)
.withMaxRegexLength(200)
.withTimeout(Duration.ofSeconds(5));
// Evaluate with sandboxed options
OclResult result = engine.evaluateWithDiagnostics(expr, context, sandboxed);
// Check for limit violations
if (result.value() == OclInvalid.INSTANCE) {
result.diagnostics().forEach(d ->
logger.warn("OCL limit violation: {}", d.getMessage()));
}How Limits Are Enforced
- Recursion depth — checked in
eval()before every expression evaluation - Collection size — checked before creating ranges (
Sequence{1..n}), products, and allInstances results - Closure iterations — checked in the worklist loop of
closure()iterator - Regex length — checked before
Pattern.compile()inmatches(),replaceAll(),replaceFirst() - Timeout — deadline-based
System.nanoTime()check at everyeval()call and every closure iteration (~15ns overhead)
Trust Boundaries
| Source | Trust Level | Recommendation |
|---|---|---|
| Own model annotations | Trusted | Default strict() options |
| External Complete OCL documents | Semi-trusted | strict() with tightened limits |
| User input (console, LSP) | Untrusted | Tightened limits + timeout |
| Custom operation providers | Controlled (D29) | Disabled by default; requires customOperationsEnabled on both Config and Options |
| EMF delegates | Explicit opt-in | Configure delegateOptions before installDelegates() |
OSGi: Engine-Wide Security Defaults via ConfigAdmin
In an OSGi environment, configure conservative defaults centrally via ConfigAdmin instead of relying on each caller to pass sandboxed options:
{
":configurator:resource-version": 1,
"DefaultOclEngine": {
"ocl.maxDepth": 100,
"ocl.maxCollectionSize": 10000,
"ocl.maxClosureIterations": 1000,
"ocl.maxRegexLength": 200,
"ocl.timeout": 5000,
"ocl.nullHandling": "STRICT",
"ocl.errorRecovery": "FAIL_FAST"
}
}These limits apply to all evaluate(expression, context) calls (without explicit options), including EMF delegate evaluations. Callers can still pass explicit OclEvaluationOptions to override for individual evaluations.
EMF Delegate Security
// Configure delegate options BEFORE installing delegates
engine.setDelegateOptions(sandboxed);
engine.installDelegates();After installDelegates(), any model loaded in the EPackage.Registry can trigger OCL evaluation via EAnnotations. Only install delegates if you trust the models being loaded.
Custom Operation Security (D29)
Custom operations are disabled by default. To enable them, set customOperationsEnabled on both the engine configuration and the per-evaluation options (AND-linked):
// Engine configuration: enable custom ops engine-wide
OclConfiguration config = OclConfiguration.builder(parserSupport)
.addOperationProvider(myProvider)
.customOperationsEnabled(true)
.build();
// Per-evaluation: enable for this specific evaluation
OclEvaluationOptions opts = OclEvaluationOptions.strict()
.withCustomOperationsEnabled(true);If either flag is false, custom operations are not dispatched. This allows sandboxing individual evaluations (e.g. user input) even when the engine has providers registered.
For the full security analysis with BSI TR-03185 mapping, see OCL Security Analysis.
