Skip to content

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

  1. Overview
  2. Quick Start
  3. Engine Setup
  4. Evaluating Expressions
  5. OclContext — Evaluation Context
  6. Evaluation Options
  7. Caching
  8. Error Handling
  9. EMF Delegate Integration
  10. Complete OCL Documents
  11. Custom Operations
  12. Thread Safety
  13. Value Type Mapping
  14. 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:

OperationSpeedup
Parse (no cache)100–315x faster
Parse (cached)~100,000x faster
Parse + Eval176–454x faster
Pure Evaluation20–59% faster

See benchmark-results.md for detailed numbers.

Maven Coordinates

GroupId: org.eclipse.fennec.m2x
BundleDescription
org.eclipse.fennec.m2x.ocl.apiPublic API interfaces
org.eclipse.fennec.m2x.ocl.parserANTLR4 parser
org.eclipse.fennec.m2x.ocl.engineEvaluator implementation
org.eclipse.fennec.m2x.ocl.modelOCL EMF metamodel

2. Quick Start

Minimal example — parse and evaluate an OCL expression against an EMF object:

java
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); // true

That's it. Three lines to set up, one line to evaluate.


3. Engine Setup

3.1 Minimal (No Cache)

java
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

java
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)
);
java
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:

java
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:

ComponentScopeWhat's shared?
OclParserSupportPROTOTYPENothing — each engine gets its own parser
OclEngineComponentPROTOTYPENothing — each consumer gets its own engine with isolated PropertyAccessorCache
DefaultOclExpressionCacheComponentSINGLETONShared 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:

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:

PropertyTypeDefaultDescription
ocl.maxDepthint1,000Maximum expression nesting depth
ocl.maxCollectionSizeint1,000,000Maximum collection elements
ocl.maxClosureIterationsint100,000Maximum closure() iterations
ocl.maxRegexLengthint1,000Maximum regex pattern length
ocl.timeoutlong0Evaluation timeout in ms (0 = no timeout)
ocl.nullHandlingStringSTRICTSTRICT or LENIENT
ocl.errorRecoveryStringFAIL_FASTFAIL_FAST or COLLECT_ERRORS
ocl.customOperationsEnabledbooleanfalseEnable 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:

json
{
    ":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:

java
@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:

java
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:

java
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:

java
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:

java
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

java
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

java
// 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

java
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:

java
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):

java
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:

  1. Engine-wide defaults — set via OclConfiguration builder (standalone) or ConfigAdmin (OSGi). Used by evaluate(expression, context).
  2. Delegate defaults — set via setDelegateOptions(). Used by EMF delegate evaluations.
  3. Per-evaluation options — passed explicitly to evaluate(expression, context, options). Override engine-wide defaults.

6.1 Presets

java
// 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:

java
OclEvaluationOptions options = OclEvaluationOptions.strict()
    .withTimeout(Duration.ofSeconds(10))
    .withMaxDepth(500)
    .withMaxCollectionSize(100_000)
    .withMaxClosureIterations(50_000)
    .withMaxRegexLength(500)
    .withUseEMFTypes(true);

6.3 Option Reference

OptionDefaultDescription
NullHandling.STRICTyesNull property access returns OclInvalid.INSTANCE
NullHandling.LENIENTNull property access returns null
ErrorRecovery.FAIL_FASTyesStop on first error
ErrorRecovery.COLLECT_ERRORSCollect all errors, continue evaluation
maxDepth1,000Maximum expression nesting depth
timeoutnoneMaximum evaluation time
maxCollectionSize1,000,000Maximum elements in a collection
maxClosureIterations100,000Maximum closure() iterations
maxRegexLength1,000Maximum regex pattern length
useEMFTypesfalseWrap 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 typeJava 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
IntegerInteger (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) → unmodifiable EList
  • Map → unmodifiable EMap

Nested collections inside the result are left untouched. Non-collection values (numbers, strings, EObjects, …) pass through unchanged.

Example

java
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:

java
OclConfiguration config = OclConfiguration.builder(parser)
    .useEMFTypes(true)
    .build();
OclEngine engine = new OclEngineImpl(config);

Delegate note: The EMF invocation delegate always returns an EList for multi-valued EOperations regardless of this flag — that is required by the EMF contract (see issue #3). The useEMFTypes flag (see issue #4) only affects the shape of OclEngine.evaluate(...) return values.


7. Caching

7.1 Expression Cache

The OclLruExpressionCache caches parsed OclExpression ASTs keyed by (expression, contextType):

java
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 keynsURI#contextTypeName#expression

7.2 WarmUp

Pre-populate the PropertyAccessorCache and parse common expressions:

java
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:

ScenarioWithout CacheWith CacheSpeedup
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:

java
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:

StateMeaningCheck
Regular valueNormal resultresult != null && !(result instanceof OclInvalid)
nullOCL void/undefinedresult == null
OclInvalid.INSTANCEOCL invalid (error)result instanceof OclInvalid

8.3 OclResult — Three States

When using evaluateWithDiagnostics():

java
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

java
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.0

The engine also serves the legacy Eclipse OCL Pivot delegate URI for interop (see 9.5):

http://www.eclipse.org/emf/2002/Ecore/OCL/Pivot

Both 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:

xml
<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:

xml
<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:

xml
<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/Pivot

This 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:

  • StandaloneinstallDelegates() registers each factory under all served URIs; uninstallDelegates() removes them all.
  • OSGi — the emf.osgi delegate registry reads emf.configuratorName as 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

java
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:

java
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:

java
List<Constraint> constraints = engine.parseDocument(oclDocument, myResourceSet);

10.4 OSGi: CompleteOclContribution

In OSGi, deploy Complete OCL documents as whiteboard services:

java
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:

java
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

java
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:

java
@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:

json
{
    "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

java
OclConfiguration config = OclConfiguration.builder(parser)
    .addOperationProvider(new MyOperations())
    .addOperationProvider(new MoreOperations())
    .build();

12. Thread Safety

ComponentThread-Safe?Notes
OclEngineImplYesStateless evaluation; shared parser and cache are synchronized
OclLruExpressionCacheYesInternally synchronized with atomic counters
OclContextYesImmutable record
OclEvaluationOptionsYesImmutable record
OclResultYesImmutable record
OclExpression (parsed AST)YesImmutable after parsing; safe to share across threads
OclParserSupportYesThread-safe parser implementation
installDelegates()NoModifies global EMF registries — call once at startup

Recommended pattern for multi-threaded use:

java
// 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 useEMFTypes option that switches top-level Collection/Map returns to EList/EMap.

OCL types map to Java types as follows:

OCL TypeJava TypeNotes
Booleanjava.lang.Boolean
Integerjava.lang.IntegerPromoted to Long for large values
Realjava.lang.Double
Stringjava.lang.String
UnlimitedNaturaljava.lang.Integer* maps to -1
OclVoidnull
OclInvalidOclInvalid.INSTANCESingleton
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
Tuplejava.util.Map<String, Object>Keys are part names
Any EClassorg.eclipse.emf.ecore.EObject
Any EEnumEMF-generated enum literal
Any EDataTypeCorresponding 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

FieldTypeDefaultProtects against
maxDepthint1,000Stack overflow via deeply nested expressions
maxCollectionSizeint1,000,000Range explosion, product explosion, allInstances
maxClosureIterationsint100,000Unbounded closure traversal
maxRegexLengthint1,000ReDoS via crafted regex patterns
timeoutDurationnoneRunaway evaluation (deadline-based enforcement)

All limits produce OclInvalid with a diagnostic error when exceeded.

Sandboxed Evaluation

java
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() in matches(), replaceAll(), replaceFirst()
  • Timeout — deadline-based System.nanoTime() check at every eval() call and every closure iteration (~15ns overhead)

Trust Boundaries

SourceTrust LevelRecommendation
Own model annotationsTrustedDefault strict() options
External Complete OCL documentsSemi-trustedstrict() with tightened limits
User input (console, LSP)UntrustedTightened limits + timeout
Custom operation providersControlled (D29)Disabled by default; requires customOperationsEnabled on both Config and Options
EMF delegatesExplicit opt-inConfigure 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:

json
{
    ":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

java
// 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):

java
// 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.

Released under the EPL-2.0 License. Eclipse Fennec is part of the Eclipse Foundation.