Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/docs/intent-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**Event-driven create-from (`generates` + `event:`, [#6711](https://github.com/eclipse-dirigible/dirigible/issues/6711)):** a `generates` entry may declare `event: { onTransition: <from>, when: "<Status> == <status>" }` (guard mandatory) or `{ onCreate: <from> }` and mint the follow-up **document — header AND items** by itself when the source reaches a state, instead of waiting for the button (`posts` is event-driven but emits flat rows and cannot reference the new header). The `map` entry copying the source's key IS the back-reference and therefore the **at-most-once** guard, derived rather than declared twice; the button is dropped unless `button: true`, and both triggers share ONE generated create-from (a new `GenerateOnEvent.java.template` listener calls `<X>Generate.create(id)` and carries no mapping of its own). Details in the engine-intent guide's `event:` bullet.

**Declarative step resilience (`retry:` / `onError:` / `{error}` / `vars:`, [#6762](https://github.com/eclipse-dirigible/dirigible/issues/6762)):** a `delegate:` service task may declare `retry: { count, every }` (a Flowable failed-job retry cycle, `R<count+1>/<every>` — the R number counts TOTAL attempts) and `onError: <step | end>` (an error boundary event routed like a decision branch). The boundary only fires on a `BpmnError`, so `engine-bpm-flowable`'s `ResilientClassDelegateFactory` converts the FINAL failed attempt (mirroring `JobRetryCmd`'s arithmetic) into the caught `INTENT_STEP_FAILED` error, publishing the failure message as the `__errorMessage` variable — which a `setField` step on the error route reads via the whole-value `{error}` token. Process-level `vars:` + step `produces:`/`uses:` declare step data (an undeclared name is a parse error) and `clearAfter: <step>` removes a value — a generated credential — from the instance data once that step completes. v1 is delegate-only (parser-enforced); absent keys generate byte-identically. Details in the engine-intent guide's step-resilience bullet.
**Declarative step resilience (`retry:` / `onError:` / `{error}` / `vars:`, [#6762](https://github.com/eclipse-dirigible/dirigible/issues/6762) + [#7056](https://github.com/eclipse-dirigible/dirigible/issues/7056)):** a `delegate:` or a `notify:` service task may declare `retry: { count, every }` (a Flowable failed-job retry cycle, `R<count+1>/<every>` — the R number counts TOTAL attempts) and `onError: <step | end>` (an error boundary event routed like a decision branch). The boundary only fires on a `BpmnError`, so `engine-bpm-flowable` converts the FINAL failed attempt (mirroring `JobRetryCmd`'s arithmetic) into the caught `INTENT_STEP_FAILED` error, publishing the failure message as the `__errorMessage` variable — which a `setField` step on the error route reads via the whole-value `{error}` token. Process-level `vars:` + step `produces:`/`uses:` declare step data (an undeclared name is a parse error) and `clearAfter: <step>` removes a value — a generated credential — from the instance data once that step completes. **The SEND was the one step whose failure had nowhere to go** ([#7056](https://github.com/eclipse-dirigible/dirigible/issues/7056)): its generated handler fails the task on a delivery error on purpose, but the conversion existed only on the `flowable:class` path, so an SMTP blink dead-lettered the job and left a record whose real work had completed sitting in its in-progress status with no message and no failure status — the incident being on the job, not the record — and the only workaround was to make the send the last step. `ResilientActivityBehaviorFactory` adds the second hook on the `flowable:delegateExpression` path (`${JavaTask}`/`${JSTask}`) by wrapping `ServiceTaskDelegateExpressionActivityBehavior`'s own `handleException`, so a `BpmnError` a delegate raises itself and a `flowable:mapException` keep the stock behaviour and only a plain unmapped failure converts; `IntentStepResilience` is unchanged. Refused at parse, each because the declaration would otherwise never fire: a `setField`/`setRelationField` step (a check-gated status write is refused synchronously to the person who acted), a `call:` or bare service task, and a **fan-out** send (`notify.forEach`), which is per-row fail-soft and never fails the task — observe that one with `outcome:` + `onNotifyFailed`. Absent keys generate byte-identically. Details in the engine-intent guide's step-resilience bullet.

**`permissions.can` is what the generated app enforces ([#6760](https://github.com/eclipse-dirigible/dirigible/issues/6760)):** the authored access model and the enforced one were two disjoint namespaces - `permissions[].role` became `<intent>.roles` while every generated controller's gate was the convention-derived `<project>.<perspective>.<Entity>FullAccess`, a name no intent construct mentions, so granting an authored role granted *nothing* and nothing errored or warned (the roles did show up in the UI, so it looked wired). `PermissionSupport` resolves the `can: [Resource:action]` tokens into per-resource read / write role sets that the EDM and report generators emit as the entity's / report's own `roleRead` / `roleWrite`: a **covered entity is gated entirely by the authored roles** (its convention roles are neither the gate nor declared, or `<intent>.roles` and the template's `default-roles.roles` would each declare the same name), a **composition child inherits the master's** grants when it has none of its own, a covered entity **no grant may write keeps a write gate no declared role satisfies** - what a read-only allow-list says - and an entity no token names is byte-identical to before. The action **vocabulary is closed**: `read`/`view`/`list` → read, `write`/`create`/`update`/`edit`/`delete`/`manage` → write **and** read (a caller who may change a record must be able to load it), `*`/`all` → both; anything else (`approve`, `start`) is a business action with no generated URL and becomes a generation **advisory** naming the token rather than a silent drop, an undeclared resource becomes an **issue**, and a malformed token is refused at **parse**. Since the gate may now name several roles, the rest-java controllers' entity/report gate went from `UserFacade.isInRole` to the any-of `isInAnyRole` the per-property `visibleTo:` machinery already used. The URL half - `<intent>.access` over the controller subtrees, generated pages and report pages the templates publish - is **opt-in through the project's `.settings`** (`{"access": {"generate": true}}`), not a DSL key, and carries method `*` on purpose: the generated controllers read through `POST .../search`, so a "POST means write" split would lock a read-only role out of every list. **A hand-authored `.access` at the project root is scrub-owned** (`.access` is an intent-owned extension) - hand-written constraints belong under `custom/`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import org.eclipse.dirigible.commons.config.DirigibleConfig;
import org.eclipse.dirigible.components.data.sources.manager.DataSourcesManager;
import org.eclipse.dirigible.components.engine.bpm.BpmProvider;
import org.eclipse.dirigible.components.engine.bpm.flowable.delegate.ResilientActivityBehaviorFactory;
import org.eclipse.dirigible.components.engine.bpm.flowable.delegate.ResilientClassDelegateFactory;
import org.eclipse.dirigible.components.engine.bpm.flowable.diagram.DirigibleProcessDiagramGenerator;
import org.eclipse.dirigible.engine.java.runtime.ClientClassLoaderHolder;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.bpmn.parser.factory.DefaultActivityBehaviorFactory;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.actuate.endpoint.ProcessEngineEndpoint;
import org.flowable.spring.boot.actuate.info.FlowableInfoContributor;
Expand Down Expand Up @@ -120,11 +120,14 @@ private SpringProcessEngineConfiguration createProcessEngineConfig(DataSource da
// (FlowableClientClassLoaderRefresher additionally evicts the parsed-process cache on rebuild).
config.setUseClassForNameClassLoading(false);

// Run every flowable:class task through the resilient ClassDelegate: a task carrying an intent
// onError error boundary has its FINAL failed attempt converted to the caught BPMN error
// (message published for {error}) instead of dead-lettering; everything else is untouched. The
// engine's initBehaviorFactory injects the expression manager into this factory later.
config.setActivityBehaviorFactory(new DefaultActivityBehaviorFactory(new ResilientClassDelegateFactory()));
// Run every service task through the resilient behaviours - the flowable:class ones through the
// resilient ClassDelegate, the flowable:delegateExpression ones (${JavaTask} / ${JSTask}, i.e.
// every generated handler) through the resilient delegate-expression behaviour: a task carrying
// an intent onError error boundary has its FINAL failed attempt converted to the caught BPMN
// error (message published for {error}) instead of dead-lettering; everything else is
// untouched. The engine's initBehaviorFactory injects the expression manager into this factory
// later.
config.setActivityBehaviorFactory(new ResilientActivityBehaviorFactory(new ResilientClassDelegateFactory()));

return config;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.engine.bpm.flowable.delegate;

import org.flowable.bpmn.model.ServiceTask;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.impl.bpmn.behavior.ServiceTaskDelegateExpressionActivityBehavior;
import org.flowable.engine.impl.bpmn.helper.ClassDelegateFactory;
import org.flowable.engine.impl.bpmn.parser.factory.DefaultActivityBehaviorFactory;

/**
* The behaviour factory the engine is configured with, so the intent DSL's {@code onError} error
* routing has its conversion hook on <em>both</em> service-task paths: the {@code flowable:class}
* one through the {@link ClassDelegateFactory} this is constructed with (which yields
* {@link ResilientClassDelegate}s), and the {@code flowable:delegateExpression} one through the
* override below (which yields {@link ResilientServiceTaskDelegateExpressionActivityBehavior}s).
*
* <p>
* Flowable exposes no {@code ClassDelegateFactory} analogue for the delegate-expression path, so
* the hook has to be the overridable factory method itself. The construction mirrors the
* superclass's own exactly - only the behaviour type differs - and {@code expressionManager} is the
* inherited field the engine's {@code initBehaviorFactory} injects into this instance after
* configuration.
*
* <p>
* Both {@code ${JavaTask}} and {@code ${JSTask}} therefore run through the conversion. That is
* wider than the DSL currently allows anyone to ask for, and deliberately harmless: the conversion
* fires only on a task carrying a boundary event catching {@code INTENT_STEP_FAILED}, which nothing
* but the intent BPMN generator emits, and which shapes may declare an {@code onError:} at all is
* decided by the intent parser.
*/
public class ResilientActivityBehaviorFactory extends DefaultActivityBehaviorFactory {

public ResilientActivityBehaviorFactory(ClassDelegateFactory classDelegateFactory) {
super(classDelegateFactory);
}

@Override
public ServiceTaskDelegateExpressionActivityBehavior createServiceTaskDelegateExpressionActivityBehavior(ServiceTask serviceTask) {
Expression delegateExpression = expressionManager.createExpression(serviceTask.getImplementation());
return new ResilientServiceTaskDelegateExpressionActivityBehavior(serviceTask.getId(), delegateExpression,
getSkipExpressionFromServiceTask(serviceTask), createFieldDeclarations(serviceTask.getFieldExtensions()),
serviceTask.getMapExceptions(), serviceTask.isTriggerable());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.engine.bpm.flowable.delegate;

import java.util.List;

import org.flowable.bpmn.model.MapExceptionEntry;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.ServiceTaskDelegateExpressionActivityBehavior;
import org.flowable.engine.impl.bpmn.parser.FieldDeclaration;

/**
* The behaviour every {@code flowable:delegateExpression} service task runs through (created by
* {@link ResilientActivityBehaviorFactory}), adding the intent DSL's step resilience to the second
* of the two service-task paths: when the resolved delegate's FINAL failed attempt happens on a
* task carrying an intent {@code onError} error boundary, the failure is converted into the caught
* BPMN error instead of dead-lettering - see {@link IntentStepResilience}.
*
* <p>
* This is the twin of {@link ResilientClassDelegate}, which covers the {@code flowable:class} path.
* It exists because the generated senders, setters, resolvers and loaders are all bound through the
* {@code ${JavaTask}} / {@code ${JSTask}} dispatchers, which never pass through
* {@code ClassDelegate} - so until dirigible #7056 a {@code notify:} step could not declare
* {@code retry:} / {@code onError:} at all, while its generated sender did fail the task on a
* delivery error (the send was the one step in a process whose failure had nowhere to go).
*
* <p>
* The hook is {@link #handleException}, the single funnel the superclass routes {@code execute},
* {@code trigger} and the future-delegate completion through. Wrapping it - rather than
* re-implementing the catch - is what keeps the stock semantics intact: the superclass propagates a
* {@link org.flowable.engine.delegate.BpmnError} the delegate threw itself and applies a matching
* {@code flowable:mapException} on its own, and only rethrows a plain, unmapped failure. That
* rethrow is exactly the failure destined for the retry cycle / dead-letter path, i.e. the same
* predicate the {@code flowable:class} twin relies on.
*/
class ResilientServiceTaskDelegateExpressionActivityBehavior extends ServiceTaskDelegateExpressionActivityBehavior {

private static final long serialVersionUID = 1L;

ResilientServiceTaskDelegateExpressionActivityBehavior(String serviceTaskId, Expression expression, Expression skipExpression,
List<FieldDeclaration> fieldDeclarations, List<MapExceptionEntry> mapExceptions, boolean triggerable) {
super(serviceTaskId, expression, skipExpression, fieldDeclarations, mapExceptions, triggerable);
}

@Override
protected void handleException(Throwable exception, DelegateExecution execution, boolean loggingSessionEnabled) {
try {
super.handleException(exception, execution, loggingSessionEnabled);
} catch (RuntimeException unhandled) {
if (!IntentStepResilience.convertFinalFailure(execution, unhandled)) {
throw unhandled;
}
}
}
}
Loading
Loading