From 69f265165c8f2547a6402d3246db309d89b0b358 Mon Sep 17 00:00:00 2001 From: ThuF Date: Thu, 10 Sep 2026 17:19:20 +0300 Subject: [PATCH] java: the '@Component JavaDelegate' rule is a publish-time Problems entry (#7291) The rule "a JavaDelegate must NOT be a @Component" was observable only as a WARN from ComponentContainer.createUnmanaged. On the ${JavaTask} + handler path that method runs for every execution of the step (the delegate is fresh per execution by design), so an annotated handler on a step that runs a thousand times a day logged a thousand identical WARNs - and none of them reached the developer who wrote the annotation, only whoever happened to read the log of a run. - ComponentContainer.rebuild now flags a bean that implements Flowable's JavaDelegate - matched by interface NAME, since engine-java cannot see the Flowable type - as a wiringWarnings() entry, carried on RebuildResult and projected by JavaSynchronizer onto the Problems view at publish. It is a warning, not a wiring error: the bean is built and usable, so the artefact stays CREATED and only the Problems entry appears. - The execution-time WARN is kept (a delegate can arrive from an AOT module the synchronizer never saw) but is logged once per class per generation and then at DEBUG, the once-then-DEBUG shape #7220/#7267 established. The suppression set is cleared by rebuild, so a republish states it again. - Its message no longer claims the class "is a JavaDelegate": the check there is isBean on whatever class was asked to be wired unmanaged, so it now says "instantiated outside the container" instead. Coverage: ComponentContainerDelegateRuleTest, with a name-only org.flowable.engine.delegate.JavaDelegate stand-in under src/test/java - which is what makes the by-name match testable without the dependency. Co-Authored-By: Claude Opus 5 --- components/engine/engine-java/CLAUDE.md | 23 +++ .../java/component/ComponentContainer.java | 90 +++++++- .../engine/java/runtime/JavaLoader.java | 8 +- .../java/synchronizer/JavaSynchronizer.java | 21 +- .../ComponentContainerDelegateRuleTest.java | 193 ++++++++++++++++++ .../engine/delegate/JavaDelegate.java | 24 +++ 6 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/component/ComponentContainerDelegateRuleTest.java create mode 100644 components/engine/engine-java/src/test/java/org/flowable/engine/delegate/JavaDelegate.java diff --git a/components/engine/engine-java/CLAUDE.md b/components/engine/engine-java/CLAUDE.md index f18a79b5799..babefddab01 100644 --- a/components/engine/engine-java/CLAUDE.md +++ b/components/engine/engine-java/CLAUDE.md @@ -114,6 +114,19 @@ One Spring-singleton container, rebuilt per `ClientClassLoader` generation. Ambiguity **refuses** rather than guessing, which is stricter than `Beans.get(SomeInterface.class)` (that answers empty and falls through to the platform context, surfacing as "no such bean"). Unit coverage: `ComponentContainerUnmanagedTest`; end-to-end: `JavaDelegateInjectionIT`. +- **"A `JavaDelegate` must NOT be a `@Component`" is checked at publish, not per execution** (#7291). + `rebuild` flags a bean that implements `org.flowable.engine.delegate.JavaDelegate` — matched by + interface **name**, since `engine-java` cannot see the Flowable type — as a `wiringWarnings()` entry, + which `JavaSynchronizer` projects onto the Problems view while leaving the artefact `CREATED` (the + bean works; the annotation is the mistake). `createUnmanaged` still WARNs, because a delegate can + reach it from an AOT module the synchronizer never saw, but **once per class per generation and then + at DEBUG**: the `${JavaTask}` path wires a fresh delegate for every execution, so an unconditional + WARN restated the same fact on every tick of a step that runs all day. The suppression set is cleared + by `rebuild`, so a republish says it again. Its message says "instantiated outside the container" + rather than "is a JavaDelegate", because the check there is `isBean` on whatever class the caller + asked to wire — it must not claim more than it looked at. Coverage: + `ComponentContainerDelegateRuleTest` (with a name-only `org.flowable.engine.delegate.JavaDelegate` + stand-in under `src/test/java`, which is how the by-name match is testable without the dependency). ## Behaviour consumers (`JavaClassConsumer` SPI) @@ -248,6 +261,9 @@ through `ClientBeanFactory.createUnmanaged` (see the container section): new generation. - **`${JavaTask}` + a `handler` field** — `DirigibleJavaCallDelegate`, fresh per execution. +A delegate annotated `@Component` is reported at **publish** as a Problems entry on its source +(`ComponentContainer.wiringWarnings()`), not as a WARN per step execution — see the container section. + Three properties worth keeping: a delegate stays **lazy** (nothing is built at publish, so an unsatisfiable dependency is a *step* failure routed by the step's `retry:` / `onError:`, never a publish-time wiring error); a class declaring **no injection point is built exactly as before**; and @@ -306,6 +322,13 @@ view and mark the `JavaFile` artefact `FAILED` (see `JavaSynchronizer.recordComp `ComponentContainer.wiringErrors()` carried on `RebuildResult`). Don't regress this — it's how a browser-IDE developer sees what's wrong without reading the server log. +**Bean-wiring warnings** (`ComponentContainer.wiringWarnings()`, also on `RebuildResult`) take the same +route to the Problems view but leave the artefact `CREATED`: the class compiled and wired, it just +breaks a container rule. Today there is one — a bean that is also a `JavaDelegate` (#7291). Reach for a +warning rather than an error whenever the code still runs correctly enough that failing the artefact +would be a lie; reach for the Problems view rather than a log line whenever the audience is the +developer who wrote the line, not the operator who happened to run the process. + ## Conventions / gotchas - `@Roles` mirrors `UserFacade.isInRole` without pulling `api-security` (which would drag diff --git a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/component/ComponentContainer.java b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/component/ComponentContainer.java index 626cd692a3e..9908fdc3621 100644 --- a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/component/ComponentContainer.java +++ b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/component/ComponentContainer.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.dirigible.engine.java.runtime.ClientBeanFactory; import org.eclipse.dirigible.engine.java.runtime.ClientBeansHolder; @@ -62,6 +63,9 @@ public class ComponentContainer implements ClientBeanFactory { private static final Logger LOGGER = LoggerFactory.getLogger(ComponentContainer.class); + /** Flowable's delegate interface, referenced by name — see {@link #isJavaDelegate(Class)}. */ + private static final String FLOWABLE_JAVA_DELEGATE = "org.flowable.engine.delegate.JavaDelegate"; + /** Definitions of the live generation, in registration order. */ private volatile List definitions = List.of(); @@ -74,6 +78,16 @@ public class ComponentContainer implements ClientBeanFactory { /** client class FQN → wiring error from the last rebuild (so the synchronizer can surface it). */ private volatile Map wiringErrors = Map.of(); + /** client class FQN → wiring warning from the last rebuild (surfaced, but not a failure). */ + private volatile Map wiringWarnings = Map.of(); + + /** + * Classes already warned about on the {@link #createUnmanaged(Class)} path in this generation, so + * the same fact is stated once and then only at DEBUG. Cleared by {@link #rebuild(Collection)}: a + * republish is a new generation, and the developer who just changed the class should hear it again. + */ + private final Set reportedUnmanagedBeans = ConcurrentHashMap.newKeySet(); + public ComponentContainer(ClientBeansHolder holder) { holder.swap(this); } @@ -90,6 +104,19 @@ public Map wiringErrors() { return wiringErrors; } + /** + * Wiring warnings from the last {@link #rebuild(Collection)} keyed by client class FQN — + * today exactly one: a bean that is also a Flowable {@code JavaDelegate}. The class still works, so + * this is deliberately not a {@link #wiringErrors() wiring error} (the artefact stays healthy); it + * is surfaced on the Problems view at publish because that is where the developer who annotated the + * class looks, whereas the execution-time WARN only reaches whoever happens to run the process. + * + * @return an immutable FQN → message map (empty if the last rebuild had nothing to warn about) + */ + public Map wiringWarnings() { + return wiringWarnings; + } + /** * Re-create the whole client bean set for a new generation. Builds and instantiates the new beans * first, publishes them atomically, then tears down the previous generation (so reads transition @@ -105,6 +132,7 @@ public synchronized void rebuild(Collection loaded) { Map byName = new LinkedHashMap<>(); List ordered = new ArrayList<>(); Map errors = new LinkedHashMap<>(); + Map warnings = new LinkedHashMap<>(); ClassLoader loader = null; for (LoadedClass info : loaded) { if (info == null) { @@ -115,6 +143,14 @@ public synchronized void rebuild(Collection loaded) { continue; } loader = info.loader(); + if (isJavaDelegate(type)) { + // The bean is still registered - the annotation is the mistake, not the class - so this + // rebuild behaves exactly as it did before the check existed, and the only new effect is + // the Problems entry the synchronizer projects from wiringWarnings(). + String message = componentOnDelegateMessage(type.getName()); + LOGGER.warn(message); + warnings.put(type.getName(), message); + } try { String name = beanName(type); BeanDefinition existing = byName.get(name); @@ -173,6 +209,8 @@ public synchronized void rebuild(Collection loaded) { this.singletons = java.util.Collections.unmodifiableMap(snapshot); this.instancesByType = java.util.Collections.unmodifiableMap(byType); this.wiringErrors = Map.copyOf(errors); + this.wiringWarnings = Map.copyOf(warnings); + reportedUnmanagedBeans.clear(); destroy(previousDefinitions, previousSingletons); LOGGER.info("Client bean container rebuilt: {} bean(s).", snapshot.size()); @@ -436,9 +474,17 @@ public List getAll(Class type) { @Override public Optional createUnmanaged(Class type) { if (isBean(type)) { - LOGGER.warn( - "[{}] is a JavaDelegate annotated @Component. A JavaDelegate must NOT be a @Component: Flowable instantiates the delegate itself, so the annotation additionally builds a container-managed singleton the engine never runs — a stray candidate for every List injection. Remove @Component from the delegate.", - type.getName()); + // Once per class per generation, then DEBUG: on the ${JavaTask} path a fresh delegate is + // wired for every execution, so an unconditional WARN would restate the same fact on every + // tick of a step that runs all day. The publish-time entry in wiringWarnings() is the one a + // developer is meant to read; this line only serves whoever is already reading the log. + if (reportedUnmanagedBeans.add(type.getName())) { + LOGGER.warn(componentOnUnmanagedMessage(type.getName())); + } else if (LOGGER.isDebugEnabled()) { + // Guarded because this runs per step execution: with DEBUG off, the suppressed repeat + // must not even build its message. + LOGGER.debug(componentOnUnmanagedMessage(type.getName())); + } } BeanDefinition definition = new BeanDefinition(type.getName(), type); if (!declaresInjectionPoint(definition)) { @@ -472,6 +518,44 @@ private static boolean isBean(Class type) { return AnnotatedElementUtils.hasAnnotation(type, Component.class); } + /** + * Whether {@code type} is a Flowable {@code JavaDelegate}, matched by interface name: + * {@code engine-java} cannot see the Flowable type ({@code engine-bpm-flowable} depends on this + * module, not the other way round), which is also why the {@link #createUnmanaged(Class)} check is + * the broader {@code isBean}. + */ + private static boolean isJavaDelegate(Class type) { + for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + for (Class implemented : current.getInterfaces()) { + if (FLOWABLE_JAVA_DELEGATE.equals(implemented.getName()) || isJavaDelegate(implemented)) { + return true; + } + } + } + return false; + } + + /** The publish-time wording: the rebuild knows the bean is a delegate, so it says so. */ + private static String componentOnDelegateMessage(String className) { + return "[" + className + "] implements " + FLOWABLE_JAVA_DELEGATE + " and is annotated @Component. A JavaDelegate" + + " must NOT be a @Component: Flowable instantiates the delegate itself, so the annotation additionally builds" + + " a container-managed singleton the engine never runs — a stray candidate for every List" + + " injection. Remove @Component from the delegate."; + } + + /** + * The execution-time wording. It says {@code instantiated outside the container} rather than + * {@code JavaDelegate}, because the detection here is {@code isBean} on whatever class the caller + * asked to wire unmanaged - true of a delegate today, but the message must not claim more than it + * actually checked. + */ + private static String componentOnUnmanagedMessage(String className) { + return "[" + className + "] is annotated @Component but is instantiated outside the container (which is what a" + + " JavaDelegate is: Flowable instantiates it itself). Such a class must NOT be a @Component: the annotation" + + " additionally builds a container-managed singleton nothing ever runs — a stray candidate for every" + + " collection injection point of its type. Remove @Component from it."; + } + private static String beanName(Class type) { Component component = AnnotatedElementUtils.findMergedAnnotation(type, Component.class); if (component != null && !component.value() diff --git a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java index 0e2dd1d418e..6d819a45768 100644 --- a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java +++ b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java @@ -184,7 +184,8 @@ public synchronized RebuildResult rebuild(List sources) { currentBytecode.putAll(effectiveBytecode); RebuildResult result = new RebuildResult(Collections.unmodifiableSet(succeeded), Collections.unmodifiableMap(failures), - Collections.unmodifiableSet(removed), Collections.unmodifiableMap(batch.diagnostics()), componentContainer.wiringErrors()); + Collections.unmodifiableSet(removed), Collections.unmodifiableMap(batch.diagnostics()), componentContainer.wiringErrors(), + componentContainer.wiringWarnings()); // Writes this cycle's fresh bytecode and deletes only source-removed FQNs. Carried-over // (failed-to-recompile) classes keep their existing .class files untouched. @@ -392,9 +393,12 @@ public record ClientSource(String project, String fqn, String source) { * @param wiringErrors per FQN → a bean-container wiring error (unsatisfied/ambiguous dependency, * construction cycle, duplicate bean name, throwing constructor) for classes that compiled * but could not be wired + * @param wiringWarnings per FQN → a bean-container wiring warning for classes that compiled and + * wired fine but break a rule (today: a bean that is also a Flowable {@code JavaDelegate}). + * Surfaced on the Problems view like an error, but it does not fail the artefact */ public record RebuildResult(Set succeededFqns, Map failures, Set unloadedFqns, - Map> diagnostics, Map wiringErrors) { + Map> diagnostics, Map wiringErrors, Map wiringWarnings) { } } diff --git a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/synchronizer/JavaSynchronizer.java b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/synchronizer/JavaSynchronizer.java index ba9edff381b..cf2d115cdb3 100644 --- a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/synchronizer/JavaSynchronizer.java +++ b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/synchronizer/JavaSynchronizer.java @@ -292,17 +292,28 @@ private boolean rebuildAll() { file.setLifecycle(ArtefactLifecycle.CREATED); file.setError(null); javaFileService.save(file); - clearCompilationProblems(file.getLocation()); + String warning = result.wiringWarnings() + .get(fqn); + if (warning != null) { + // Compiled and wired, but breaks a container rule (today: a bean that is also a + // JavaDelegate). The artefact stays CREATED - it works - yet the entry lands in the + // Problems view at publish, in front of the developer who wrote the annotation, + // rather than only in a WARN whoever runs the process later may or may not read. + recordCompilationProblems(file.getLocation(), List.of(), warning); + } else { + clearCompilationProblems(file.getLocation()); + } } } return true; } /** - * Project a file's compile failure onto the Problems view: replace its previous compilation - * problems (so resolved errors disappear), then add one entry per structured diagnostic at its - * line/column - or a single entry with the formatted message when no positioned diagnostic is - * available (e.g. a read failure or a class that compiled but failed to load). + * Project a file's compile failure - or a wiring error/warning - onto the Problems view: replace + * its previous compilation problems (so resolved ones disappear), then add one entry per structured + * diagnostic at its line/column - or a single entry with the formatted message when no positioned + * diagnostic is available (e.g. a read failure, a class that compiled but failed to load, or a bean + * that broke a container rule). */ private void recordCompilationProblems(String location, List diagnostics, String message) { try { diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/component/ComponentContainerDelegateRuleTest.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/component/ComponentContainerDelegateRuleTest.java new file mode 100644 index 00000000000..d87cc59077e --- /dev/null +++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/component/ComponentContainerDelegateRuleTest.java @@ -0,0 +1,193 @@ +/* + * 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.engine.java.component; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.dirigible.engine.java.runtime.ClientBeansHolder; +import org.eclipse.dirigible.engine.java.spi.LoadedClass; +import org.eclipse.dirigible.sdk.component.Component; +import org.eclipse.dirigible.sdk.component.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + +/** + * "A {@code JavaDelegate} must NOT be a {@code @Component}" — where and how often the container + * states that rule. It belongs on the Problems view at publish, in front of the developer who wrote + * the annotation; the execution-time WARN is a fallback that must not restate the same fact on + * every step execution. + */ +class ComponentContainerDelegateRuleTest { + + private Logger logger; + private ListAppender appender; + + @BeforeEach + void captureContainerLog() { + logger = (Logger) LoggerFactory.getLogger(ComponentContainer.class); + appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + // The suppressed repeats are DEBUG, so the assertions can only see them with DEBUG enabled. + logger.setLevel(Level.DEBUG); + } + + @AfterEach + void releaseContainerLog() { + logger.detachAppender(appender); + logger.setLevel(null); + } + + @Test + void a_bean_that_is_also_a_delegate_is_a_wiring_warning_of_the_generation() { + ComponentContainer container = TestComponentContainers.of(RateProvider.class, ComponentDelegate.class); + + String warning = container.wiringWarnings() + .get(ComponentDelegate.class.getName()); + + assertEquals(List.of(ComponentDelegate.class.getName()), List.copyOf(container.wiringWarnings() + .keySet())); + assertTrue(warning.contains(ComponentDelegate.class.getName()), warning); + assertTrue(warning.contains("must NOT be a @Component"), warning); + assertTrue(warning.contains("org.flowable.engine.delegate.JavaDelegate"), warning); + } + + @Test + void the_warning_does_not_fail_the_bean_it_is_about() { + // It is a warning and not a wiring error on purpose: the bean is built and usable, so the + // artefact must not go FAILED - only the Problems entry appears. + ComponentContainer container = TestComponentContainers.of(RateProvider.class, ComponentDelegate.class); + + assertTrue(container.instanceOf(ComponentDelegate.class) + .isPresent()); + assertTrue(container.wiringErrors() + .isEmpty()); + } + + @Test + void a_delegate_inherited_through_a_super_interface_is_still_found() { + ComponentContainer container = TestComponentContainers.of(IndirectComponentDelegate.class); + + assertTrue(container.wiringWarnings() + .containsKey(IndirectComponentDelegate.class.getName())); + } + + @Test + void a_bean_that_is_not_a_delegate_is_not_warned_about() { + ComponentContainer container = TestComponentContainers.of(RateProvider.class); + + assertTrue(container.wiringWarnings() + .isEmpty()); + } + + @Test + void a_removed_annotation_clears_the_warning_of_the_previous_generation() { + ComponentContainer container = new ComponentContainer(new ClientBeansHolder()); + container.rebuild(generation(RateProvider.class, ComponentDelegate.class)); + + container.rebuild(generation(RateProvider.class)); + + assertTrue(container.wiringWarnings() + .isEmpty()); + } + + @Test + void the_execution_time_warning_is_logged_once_per_class_per_generation() { + ComponentContainer container = TestComponentContainers.of(RateProvider.class, ComponentDelegate.class); + appender.list.clear(); // the rebuild's own publish-time WARN is asserted separately + + container.createUnmanaged(ComponentDelegate.class); + container.createUnmanaged(ComponentDelegate.class); + container.createUnmanaged(ComponentDelegate.class); + + // On the ${JavaTask} path this runs for every execution of the step, forever. + assertEquals(1, ruleLines(Level.WARN).size(), () -> "expected exactly one WARN, got: " + appender.list); + assertEquals(2, ruleLines(Level.DEBUG).size(), () -> "expected the repeats at DEBUG, got: " + appender.list); + } + + @Test + void the_execution_time_warning_names_the_class_and_the_rule() { + ComponentContainer container = TestComponentContainers.of(RateProvider.class, ComponentDelegate.class); + appender.list.clear(); + + container.createUnmanaged(ComponentDelegate.class); + + String message = ruleLines(Level.WARN).get(0); + assertTrue(message.contains(ComponentDelegate.class.getName()), message); + assertTrue(message.contains("must NOT be a @Component"), message); + // It checked @Component on a class being wired unmanaged, not the JavaDelegate interface, so + // it must not claim the class is a delegate. + assertFalse(message.contains("is a JavaDelegate annotated"), message); + } + + @Test + void a_republish_says_it_again_because_the_developer_just_changed_the_class() { + ComponentContainer container = new ComponentContainer(new ClientBeansHolder()); + container.rebuild(generation(RateProvider.class, ComponentDelegate.class)); + container.createUnmanaged(ComponentDelegate.class); + + container.rebuild(generation(RateProvider.class, ComponentDelegate.class)); + appender.list.clear(); + container.createUnmanaged(ComponentDelegate.class); + + assertEquals(1, ruleLines(Level.WARN).size(), () -> "expected the new generation to warn again, got: " + appender.list); + } + + /** + * The captured messages of {@code level} that state the rule (not e.g. the rebuilt-container INFO). + */ + private List ruleLines(Level level) { + return appender.list.stream() + .filter(event -> event.getLevel() == level) + .map(ILoggingEvent::getFormattedMessage) + .filter(message -> message.contains("must NOT be a @Component")) + .toList(); + } + + private static List generation(Class... classes) { + return Arrays.stream(classes) + .map(type -> new LoadedClass("p", type.getName(), type, type.getClassLoader())) + .toList(); + } + + // --- fixtures -------------------------------------------------------------------------------- + + @Component + static class RateProvider { + } + + /** The mistake the rule forbids: a delegate annotated {@code @Component}. */ + @Component + static class ComponentDelegate implements org.flowable.engine.delegate.JavaDelegate { + + @Inject + RateProvider rates; + } + + interface AuditedDelegate extends org.flowable.engine.delegate.JavaDelegate { + } + + /** Same mistake, one interface further away — the search has to walk the hierarchy. */ + @Component + static class IndirectComponentDelegate implements AuditedDelegate { + } +} diff --git a/components/engine/engine-java/src/test/java/org/flowable/engine/delegate/JavaDelegate.java b/components/engine/engine-java/src/test/java/org/flowable/engine/delegate/JavaDelegate.java new file mode 100644 index 00000000000..be24fb0063c --- /dev/null +++ b/components/engine/engine-java/src/test/java/org/flowable/engine/delegate/JavaDelegate.java @@ -0,0 +1,24 @@ +/* + * 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.flowable.engine.delegate; + +/** + * A test-only stand-in for Flowable's delegate interface, under Flowable's own package so its + * {@linkplain Class#getName() binary name} is the real one. + * + *

+ * {@code engine-java} has no Flowable dependency (the dependency runs the other way: + * {@code engine-bpm-flowable} depends on this module), which is why + * {@code ComponentContainer.isJavaDelegate} matches the interface by name. This fixture is + * what lets that match be tested at all — and it deliberately declares no {@code execute} method, + * because the name is the whole of what the container checks. + */ +public interface JavaDelegate { +}