diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java index 6a7afd6324b..7ff781212c7 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java @@ -35,6 +35,7 @@ import org.flowable.bpmn.model.CancelEventDefinition; import org.flowable.bpmn.model.CompensateEventDefinition; import org.flowable.bpmn.model.ConditionalEventDefinition; +import org.flowable.bpmn.model.CustomBpmnEventDefinition; import org.flowable.bpmn.model.DataAssociation; import org.flowable.bpmn.model.DataObject; import org.flowable.bpmn.model.ErrorEventDefinition; @@ -60,7 +61,6 @@ import org.flowable.bpmn.model.TimerEventDefinition; import org.flowable.bpmn.model.UserTask; import org.flowable.bpmn.model.ValuedDataObject; -import org.flowable.bpmn.model.VariableListenerEventDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -438,7 +438,13 @@ protected boolean writeListeners(BaseElement element, boolean didWriteExtensionS } protected void writeEventDefinitions(Event parentEvent, List eventDefinitions, BpmnModel model, XMLStreamWriter xtw) throws Exception { + // Custom (non-BPMN-spec) event definitions are emitted in writeExtensionChildElements via + // BpmnXMLUtil.writeCustomEventDefinitionExtensionElements. Only the BPMN-spec types are written here + // as direct children of the event element. for (EventDefinition eventDefinition : eventDefinitions) { + if (eventDefinition instanceof CustomBpmnEventDefinition) { + continue; + } if (eventDefinition instanceof TimerEventDefinition) { writeTimerDefinition(parentEvent, (TimerEventDefinition) eventDefinition, model, xtw); @@ -653,32 +659,6 @@ protected void writeTerminateDefinition(Event parentEvent, TerminateEventDefinit xtw.writeEndElement(); } - protected boolean writeVariableListenerDefinition(Event parentEvent, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { - if (parentEvent.getEventDefinitions().size() == 1) { - EventDefinition eventDefinition = parentEvent.getEventDefinitions().iterator().next(); - if (eventDefinition instanceof VariableListenerEventDefinition variableListenerEventDefinition) { - if (!didWriteExtensionStartElement) { - xtw.writeStartElement(ELEMENT_EXTENSIONS); - didWriteExtensionStartElement = true; - } - - xtw.writeStartElement(FLOWABLE_EXTENSIONS_PREFIX, ELEMENT_EVENT_VARIABLELISTENERDEFINITION, FLOWABLE_EXTENSIONS_NAMESPACE); - - if (StringUtils.isNotEmpty(variableListenerEventDefinition.getVariableName())) { - writeDefaultAttribute(ATTRIBUTE_VARIABLE_NAME, variableListenerEventDefinition.getVariableName(), xtw); - } - - if (StringUtils.isNotEmpty(variableListenerEventDefinition.getVariableChangeType())) { - writeDefaultAttribute(ATTRIBUTE_VARIABLE_CHANGE_TYPE, variableListenerEventDefinition.getVariableChangeType(), xtw); - } - - xtw.writeEndElement(); - } - } - - return didWriteExtensionStartElement; - } - protected void writeDefaultAttribute(String attributeName, String value, XMLStreamWriter xtw) throws Exception { BpmnXMLUtil.writeDefaultAttribute(attributeName, value, xtw); } diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BoundaryEventXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BoundaryEventXMLConverter.java index 09171cfa54e..003524bdd55 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BoundaryEventXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BoundaryEventXMLConverter.java @@ -21,7 +21,6 @@ import javax.xml.stream.XMLStreamWriter; import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.converter.child.BaseChildElementParser; import org.flowable.bpmn.converter.child.InParameterParser; import org.flowable.bpmn.converter.child.VariableListenerEventDefinitionParser; @@ -32,7 +31,6 @@ import org.flowable.bpmn.model.ErrorEventDefinition; import org.flowable.bpmn.model.EventDefinition; import org.flowable.bpmn.model.ExtensionAttribute; -import org.flowable.bpmn.model.ExtensionElement; /** * @author Tijs Rademakers @@ -108,15 +106,6 @@ protected void writeAdditionalAttributes(BaseElement element, BpmnModel model, X if (!(eventDef instanceof ErrorEventDefinition)) { writeDefaultAttribute(ATTRIBUTE_BOUNDARY_CANCELACTIVITY, String.valueOf(boundaryEvent.isCancelActivity()).toLowerCase(), xtw); } - - } else if (!boundaryEvent.getExtensionElements().isEmpty()) { - List eventTypeExtensionElements = boundaryEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeExtensionElements != null && !eventTypeExtensionElements.isEmpty()) { - String eventTypeValue = eventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventTypeValue)) { - writeDefaultAttribute(ATTRIBUTE_BOUNDARY_CANCELACTIVITY, String.valueOf(boundaryEvent.isCancelActivity()).toLowerCase(), xtw); - } - } } } @@ -124,7 +113,7 @@ protected void writeAdditionalAttributes(BaseElement element, BpmnModel model, X protected boolean writeExtensionChildElements(BaseElement element, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { BoundaryEvent boundaryEvent = (BoundaryEvent) element; didWriteExtensionStartElement = BpmnXMLUtil.writeIOParameters(ELEMENT_IN_PARAMETERS, boundaryEvent.getInParameters(), didWriteExtensionStartElement, xtw); - didWriteExtensionStartElement = writeVariableListenerDefinition(boundaryEvent, didWriteExtensionStartElement, xtw); + didWriteExtensionStartElement = BpmnXMLUtil.writeCustomEventDefinitionExtensionElements(boundaryEvent, didWriteExtensionStartElement, xtw); return didWriteExtensionStartElement; } diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CatchEventXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CatchEventXMLConverter.java index f95c63e35a6..34c4b41a597 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CatchEventXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CatchEventXMLConverter.java @@ -72,8 +72,7 @@ protected void writeAdditionalAttributes(BaseElement element, BpmnModel model, X @Override protected boolean writeExtensionChildElements(BaseElement element, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { IntermediateCatchEvent catchEvent = (IntermediateCatchEvent) element; - didWriteExtensionStartElement = writeVariableListenerDefinition(catchEvent, didWriteExtensionStartElement, xtw); - return didWriteExtensionStartElement; + return BpmnXMLUtil.writeCustomEventDefinitionExtensionElements(catchEvent, didWriteExtensionStartElement, xtw); } @Override diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CustomEventDefinitionXmlWriter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CustomEventDefinitionXmlWriter.java new file mode 100644 index 00000000000..f44eb1de002 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/CustomEventDefinitionXmlWriter.java @@ -0,0 +1,31 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.converter; + +import javax.xml.stream.XMLStreamWriter; + +import org.flowable.bpmn.model.Event; +import org.flowable.bpmn.model.EventDefinition; + +/** + * Hook for serializing a {@link org.flowable.bpmn.model.CustomBpmnEventDefinition} back to XML — the + * write-side counterpart to {@link org.flowable.bpmn.converter.child.BaseChildElementParser} on the read + * side. A custom {@link EventDefinition} requires one parser (read) and one writer (write) to round-trip + * through {@code BpmnXMLConverter}. The writer is invoked inside an already-opened + * {@code } wrapper. + */ +@FunctionalInterface +public interface CustomEventDefinitionXmlWriter { + + void write(Event parentEvent, EventDefinition eventDefinition, XMLStreamWriter xtw) throws Exception; +} diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/StartEventXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/StartEventXMLConverter.java index 5170bcc5e38..70ff62525bd 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/StartEventXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/StartEventXMLConverter.java @@ -117,8 +117,7 @@ protected void writeAdditionalAttributes(BaseElement element, BpmnModel model, X writeQualifiedAttribute(ATTRIBUTE_SAME_DEPLOYMENT, "false", xtw); } - if ((startEvent.getEventDefinitions() != null && startEvent.getEventDefinitions().size() > 0) || - (startEvent.getExtensionElements() != null && startEvent.getExtensionElements().containsKey(ELEMENT_EVENT_TYPE))) { + if (startEvent.getEventDefinitions() != null && !startEvent.getEventDefinitions().isEmpty()) { writeDefaultAttribute(ATTRIBUTE_EVENT_START_INTERRUPTING, String.valueOf(startEvent.isInterrupting()), xtw); } } @@ -127,7 +126,7 @@ protected void writeAdditionalAttributes(BaseElement element, BpmnModel model, X protected boolean writeExtensionChildElements(BaseElement element, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { StartEvent startEvent = (StartEvent) element; didWriteExtensionStartElement = BpmnXMLUtil.writeIOParameters(ELEMENT_IN_PARAMETERS, startEvent.getInParameters(), didWriteExtensionStartElement, xtw); - didWriteExtensionStartElement = writeVariableListenerDefinition(startEvent, didWriteExtensionStartElement, xtw); + didWriteExtensionStartElement = BpmnXMLUtil.writeCustomEventDefinitionExtensionElements(startEvent, didWriteExtensionStartElement, xtw); didWriteExtensionStartElement = writeFormProperties(startEvent, didWriteExtensionStartElement, xtw); return didWriteExtensionStartElement; } diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/EventRegistryEventTypeParser.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/EventRegistryEventTypeParser.java new file mode 100644 index 00000000000..d9a6d7c6559 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/EventRegistryEventTypeParser.java @@ -0,0 +1,71 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.converter.child; + +import javax.xml.stream.XMLStreamReader; + +import org.apache.commons.lang3.StringUtils; +import org.flowable.bpmn.model.BaseElement; +import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.Event; +import org.flowable.bpmn.model.EventRegistryEventDefinition; +import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.bpmn.model.StartEvent; + +/** + * Reads {@code X} as a child of an event host (start / + * intermediate-catch / boundary) and produces a typed {@link EventRegistryEventDefinition} on the host's + * {@code eventDefinitions} list. The {@link EventRegistryEventDefinition} is the single source of truth in + * the model — the legacy XML form is re-emitted on serialization by the host's XML writer, no parallel + * extension-element representation is kept. + *

+ * On non-event hosts (e.g. {@code receiveTask}, {@code sendEventServiceTask}, {@code process}) this parser + * declines via {@link #accepts(BaseElement)} so {@code BpmnXMLUtil.parseChildElements} falls through to the + * generic extension-element path and existing behavior is preserved. + */ +public class EventRegistryEventTypeParser extends BaseChildElementParser { + + @Override + public String getElementName() { + return ELEMENT_EVENT_TYPE; + } + + @Override + public boolean accepts(BaseElement element) { + return element instanceof IntermediateCatchEvent + || element instanceof BoundaryEvent + || element instanceof StartEvent; + } + + @Override + public void parseChildElement(XMLStreamReader xtr, BaseElement parentElement, BpmnModel model) throws Exception { + if (!(parentElement instanceof Event event)) { + return; + } + // An empty on an event host is almost always an authoring mistake — warn so it + // surfaces at parse time rather than failing silently at runtime. + String eventTypeValue = xtr.getElementText(); + if (StringUtils.isEmpty(eventTypeValue)) { + LOGGER.warn("Empty extension element on event '{}'; ignoring (no event-registry subscription will be created)", + event.getId()); + return; + } + if (event.getEventDefinitions().stream().anyMatch(EventRegistryEventDefinition.class::isInstance)) { + LOGGER.warn("Multiple extension elements on event '{}'; only the first is used and the duplicate value '{}' is ignored", + event.getId(), eventTypeValue); + return; + } + event.addEventDefinition(new EventRegistryEventDefinition(eventTypeValue)); + } +} diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/util/BpmnXMLUtil.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/util/BpmnXMLUtil.java index d163ad49a8e..066b79ddacb 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/util/BpmnXMLUtil.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/util/BpmnXMLUtil.java @@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.converter.CustomEventDefinitionXmlWriter; import org.flowable.bpmn.converter.child.BaseChildElementParser; import org.flowable.bpmn.converter.child.CancelEventDefinitionParser; import org.flowable.bpmn.converter.child.CompensateEventDefinitionParser; @@ -40,6 +41,7 @@ import org.flowable.bpmn.converter.child.ElementNameParser; import org.flowable.bpmn.converter.child.ErrorEventDefinitionParser; import org.flowable.bpmn.converter.child.EscalationEventDefinitionParser; +import org.flowable.bpmn.converter.child.EventRegistryEventTypeParser; import org.flowable.bpmn.converter.child.ExecutionListenerParser; import org.flowable.bpmn.converter.child.FieldExtensionParser; import org.flowable.bpmn.converter.child.FlowNodeRefParser; @@ -64,16 +66,40 @@ import org.flowable.bpmn.converter.child.TimerEventDefinitionParser; import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.CustomBpmnEventDefinition; +import org.flowable.bpmn.model.Event; +import org.flowable.bpmn.model.EventDefinition; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionAttribute; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.GraphicInfo; import org.flowable.bpmn.model.IOParameter; +import org.flowable.bpmn.model.VariableListenerEventDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class BpmnXMLUtil implements BpmnXMLConstants { + private static final Logger LOGGER = LoggerFactory.getLogger(BpmnXMLUtil.class); + private static Map genericChildParserMap = new HashMap<>(); + /** + * Registry of writers for {@link CustomBpmnEventDefinition} subclasses, looked up by exact class. The + * write-side counterpart to {@link #genericChildParserMap}. The Flowable-specific + * {@link EventRegistryEventDefinition} and {@link VariableListenerEventDefinition} are pre-registered; + * additional types must register their writer at engine init. + *

+ * Writers are invoked from {@link #writeCustomEventDefinitionExtensionElements} and write inside the + * {@code } wrapper which is opened lazily by that helper. + *

+ * Lifecycle: JVM-wide static. Last-writer-wins. + * Tests that build multiple engines should call {@link #removeCustomEventDefinitionWriter(Class)} between + * runs. + */ + private static final Map, CustomEventDefinitionXmlWriter> CUSTOM_EVENT_DEFINITION_WRITERS = new HashMap<>(); + static { addGenericParser(new CancelEventDefinitionParser()); addGenericParser(new CompensateEventDefinitionParser()); @@ -86,6 +112,7 @@ public class BpmnXMLUtil implements BpmnXMLConstants { addGenericParser(new DocumentationParser()); addGenericParser(new ErrorEventDefinitionParser()); addGenericParser(new EscalationEventDefinitionParser()); + addGenericParser(new EventRegistryEventTypeParser()); addGenericParser(new ExecutionListenerParser()); addGenericParser(new FieldExtensionParser()); addGenericParser(new ScriptInfoParser()); @@ -107,12 +134,90 @@ public class BpmnXMLUtil implements BpmnXMLConstants { addGenericParser(new FlowableFailedjobRetryParser()); addGenericParser(new FlowableMapExceptionParser()); addGenericParser(new ElementNameParser()); + + // Built-in writers for the Flowable-specific (non-BPMN-spec) event definitions. + CUSTOM_EVENT_DEFINITION_WRITERS.put(EventRegistryEventDefinition.class, BpmnXMLUtil::writeEventRegistryEventDefinition); + CUSTOM_EVENT_DEFINITION_WRITERS.put(VariableListenerEventDefinition.class, BpmnXMLUtil::writeVariableListenerEventDefinition); } private static void addGenericParser(BaseChildElementParser parser) { genericChildParserMap.put(parser.getElementName(), parser); } + private static void writeEventRegistryEventDefinition(Event parentEvent, EventDefinition eventDefinition, XMLStreamWriter xtw) throws Exception { + EventRegistryEventDefinition eventRegistry = (EventRegistryEventDefinition) eventDefinition; + if (StringUtils.isEmpty(eventRegistry.getEventDefinitionKey())) { + LOGGER.warn("EventRegistryEventDefinition on event '{}' has an empty eventDefinitionKey; skipping serialization. " + + "The element will be missing from the written XML and will not round-trip.", parentEvent.getId()); + return; + } + xtw.writeStartElement(FLOWABLE_EXTENSIONS_PREFIX, ELEMENT_EVENT_TYPE, FLOWABLE_EXTENSIONS_NAMESPACE); + xtw.writeCharacters(eventRegistry.getEventDefinitionKey()); + xtw.writeEndElement(); + } + + private static void writeVariableListenerEventDefinition(Event parentEvent, EventDefinition eventDefinition, XMLStreamWriter xtw) throws Exception { + VariableListenerEventDefinition variableListener = (VariableListenerEventDefinition) eventDefinition; + xtw.writeStartElement(FLOWABLE_EXTENSIONS_PREFIX, ELEMENT_EVENT_VARIABLELISTENERDEFINITION, FLOWABLE_EXTENSIONS_NAMESPACE); + if (StringUtils.isNotEmpty(variableListener.getVariableName())) { + writeDefaultAttribute(ATTRIBUTE_VARIABLE_NAME, variableListener.getVariableName(), xtw); + } + if (StringUtils.isNotEmpty(variableListener.getVariableChangeType())) { + writeDefaultAttribute(ATTRIBUTE_VARIABLE_CHANGE_TYPE, variableListener.getVariableChangeType(), xtw); + } + xtw.writeEndElement(); + } + + /** + * Registers a {@link BaseChildElementParser} so that the parser is invoked whenever an XML element matching + * {@link BaseChildElementParser#getElementName()} is encountered as a child of a BPMN element. Intended for + * registering custom BPMN element types (e.g. a custom {@code EventDefinition}). + *

+ * Lifecycle: the parser registry is a JVM-wide static map shared by every {@link BpmnXMLConverter} + * instance in the process. Registration is last-writer-wins: a second {@code addChildElementParser} call + * for the same element name silently overwrites the previous one. Two engines in the same JVM with + * different parsers under the same XML element name will collide on the second engine's + * {@code initBpmnParser()}. Acceptable for the single-engine deployment scenario; tests that build + * multiple engines should call {@link #removeChildElementParser(String)} between runs to keep the + * registry clean. + */ + public static void addChildElementParser(BaseChildElementParser parser) { + addGenericParser(parser); + } + + /** + * Removes a previously-registered {@link BaseChildElementParser} by element name. Intended for tests that + * tear down between runs to avoid cross-test bleed; not for production use. + */ + public static void removeChildElementParser(String elementName) { + genericChildParserMap.remove(elementName); + } + + /** + * Registers an XML writer for a {@link CustomBpmnEventDefinition} subclass so that the BPMN writer can + * serialize instances of {@code eventDefinitionClass} back to XML inside {@code }. + * Combined with {@link #addChildElementParser(BaseChildElementParser)} on the read side, this gives + * custom {@link EventDefinition} types end-to-end round-trip support. + */ + public static void addCustomEventDefinitionWriter(Class eventDefinitionClass, CustomEventDefinitionXmlWriter writer) { + CUSTOM_EVENT_DEFINITION_WRITERS.put(eventDefinitionClass, writer); + } + + /** + * Removes a previously-registered {@link CustomEventDefinitionXmlWriter}. Intended for tests that tear + * down between runs to avoid cross-test bleed; not for production use. + */ + public static void removeCustomEventDefinitionWriter(Class eventDefinitionClass) { + CUSTOM_EVENT_DEFINITION_WRITERS.remove(eventDefinitionClass); + } + + /** + * Returns the writer registered for the given {@link EventDefinition} subclass, or {@code null} if none. + */ + public static CustomEventDefinitionXmlWriter findCustomEventDefinitionWriter(Class eventDefinitionClass) { + return CUSTOM_EVENT_DEFINITION_WRITERS.get(eventDefinitionClass); + } + public static void addXMLLocation(BaseElement element, XMLStreamReader xtr) { Location location = xtr.getLocation(); element.setXmlRowNumber(location.getLineNumber()); @@ -446,6 +551,36 @@ public static boolean writeElementNameExtensionElement(FlowElement element, bool return didWriteExtensionStartElement; } + /** + * Iterates over the event's {@link EventDefinition}s and writes any {@link CustomBpmnEventDefinition} using + * the writer registered for its exact class. The writers emit elements inside {@code }; + * this helper opens that wrapper if any custom definition is encountered and lazily updates + * {@code didWriteExtensionStartElement}. + */ + public static boolean writeCustomEventDefinitionExtensionElements(Event event, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { + if (event == null || event.getEventDefinitions() == null) { + return didWriteExtensionStartElement; + } + for (EventDefinition eventDefinition : event.getEventDefinitions()) { + if (!(eventDefinition instanceof CustomBpmnEventDefinition)) { + continue; + } + CustomEventDefinitionXmlWriter writer = CUSTOM_EVENT_DEFINITION_WRITERS.get(eventDefinition.getClass()); + if (writer == null) { + LOGGER.warn("No CustomEventDefinitionXmlWriter registered for {} on event '{}'; the custom event definition will be omitted from the serialized XML. " + + "Register a writer via BpmnXMLUtil.addCustomEventDefinitionWriter(...) to round-trip this type.", + eventDefinition.getClass().getName(), event.getId()); + continue; + } + if (!didWriteExtensionStartElement) { + xtw.writeStartElement(ELEMENT_EXTENSIONS); + didWriteExtensionStartElement = true; + } + writer.write(event, eventDefinition, xtw); + } + return didWriteExtensionStartElement; + } + public static boolean writeIOParameters(String elementName, List parameterList, boolean didWriteExtensionStartElement, XMLStreamWriter xtw) throws Exception { if (parameterList == null || parameterList.isEmpty()) { diff --git a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/IntermediateCatchEventRegistryEventConverterTest.java b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/IntermediateCatchEventRegistryEventConverterTest.java new file mode 100644 index 00000000000..6200239dec2 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/IntermediateCatchEventRegistryEventConverterTest.java @@ -0,0 +1,40 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.editor.language.xml; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EventRegistryEventDefinition; +import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.editor.language.xml.util.BpmnXmlConverterTest; + +class IntermediateCatchEventRegistryEventConverterTest { + + @BpmnXmlConverterTest("intermediateCatchEventRegistryEvent.bpmn") + void validateModel(BpmnModel model) { + IntermediateCatchEvent catchEvent = (IntermediateCatchEvent) model.getMainProcess().getFlowElement("catch"); + assertThat(catchEvent.getEventDefinitions()) + .singleElement() + .isInstanceOfSatisfying(EventRegistryEventDefinition.class, + def -> assertThat(def.getEventDefinitionKey()).isEqualTo("orderShipped")); + } + + @BpmnXmlConverterTest("intermediateCatchEventRegistryEvent.empty.bpmn") + void emptyEventTypeProducesNoEventDefinition(BpmnModel model) { + // An empty on an event host produces no EventDefinition (parser warns and + // skips); the writer therefore emits nothing for it on round-trip either. + IntermediateCatchEvent catchEvent = (IntermediateCatchEvent) model.getMainProcess().getFlowElement("catch"); + assertThat(catchEvent.getEventDefinitions()).isEmpty(); + } +} diff --git a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/StartEventRegistryEventConverterTest.java b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/StartEventRegistryEventConverterTest.java index 26e37a3b363..9cb63945002 100644 --- a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/StartEventRegistryEventConverterTest.java +++ b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/StartEventRegistryEventConverterTest.java @@ -16,9 +16,9 @@ import static org.assertj.core.groups.Tuple.tuple; import static org.flowable.bpmn.constants.BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER; import static org.flowable.bpmn.constants.BpmnXMLConstants.ELEMENT_EVENT_OUT_PARAMETER; -import static org.flowable.bpmn.constants.BpmnXMLConstants.ELEMENT_EVENT_TYPE; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.StartEvent; @@ -35,8 +35,10 @@ void validateModel(BpmnModel model) { assertThat(flowElement).isInstanceOfSatisfying(StartEvent.class, start -> { assertThat(start.getId()).isEqualTo("start"); assertThat(start.isInterrupting()).isEqualTo(true); - assertThat(start.getExtensionElements().get(ELEMENT_EVENT_TYPE)).extracting(ExtensionElement::getElementText) - .containsExactly("eventType1"); + assertThat(start.getEventDefinitions()) + .singleElement() + .isInstanceOfSatisfying(EventRegistryEventDefinition.class, + def -> assertThat(def.getEventDefinitionKey()).isEqualTo("eventType1")); assertThat(start.getExtensionElements().get("eventName")).extracting(ExtensionElement::getElementText) .containsExactly("eventName1"); assertThat(start.getExtensionElements().get(ELEMENT_EVENT_OUT_PARAMETER)).extracting( @@ -64,9 +66,11 @@ void validateModel(BpmnModel model) { assertThat(flowElement).isInstanceOfSatisfying(StartEvent.class, start -> { assertThat(start.getId()).isEqualTo("subProcessStart"); assertThat(start.isInterrupting()).isEqualTo(false); - assertThat(start.getExtensionElements()).containsOnlyKeys(ELEMENT_EVENT_TYPE); - assertThat(start.getExtensionElements().get(ELEMENT_EVENT_TYPE)).extracting(ExtensionElement::getElementText) - .containsExactly("eventType2"); + assertThat(start.getExtensionElements()).isEmpty(); + assertThat(start.getEventDefinitions()) + .singleElement() + .isInstanceOfSatisfying(EventRegistryEventDefinition.class, + def -> assertThat(def.getEventDefinitionKey()).isEqualTo("eventType2")); }); } } \ No newline at end of file diff --git a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/UserTaskWithEventRegistryBoundaryEventConverterTest.java b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/UserTaskWithEventRegistryBoundaryEventConverterTest.java index afdb103cc39..64fbb515de4 100644 --- a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/UserTaskWithEventRegistryBoundaryEventConverterTest.java +++ b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/UserTaskWithEventRegistryBoundaryEventConverterTest.java @@ -16,6 +16,7 @@ import org.flowable.bpmn.model.BoundaryEvent; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.UserTask; @@ -38,10 +39,12 @@ void validateModel(BpmnModel model) { .isInstanceOfSatisfying(BoundaryEvent.class, boundaryEvent -> { assertThat(boundaryEvent.getId()).isEqualTo("eventRegistryEvent"); assertThat(boundaryEvent.getAttachedToRefId()).isEqualTo("usertask"); - assertThat(boundaryEvent.getExtensionElements()).hasSize(2); - ExtensionElement extensionElement = boundaryEvent.getExtensionElements().get("eventType").get(0); - assertThat(extensionElement.getElementText()).isEqualTo("myEvent"); - extensionElement = boundaryEvent.getExtensionElements().get("eventCorrelationParameter").get(0); + assertThat(boundaryEvent.getEventDefinitions()) + .singleElement() + .isInstanceOfSatisfying(EventRegistryEventDefinition.class, + def -> assertThat(def.getEventDefinitionKey()).isEqualTo("myEvent")); + assertThat(boundaryEvent.getExtensionElements()).hasSize(1); + ExtensionElement extensionElement = boundaryEvent.getExtensionElements().get("eventCorrelationParameter").get(0); assertThat(extensionElement.getAttributeValue(null, "name")).isEqualTo("customerId"); assertThat(extensionElement.getAttributeValue(null, "value")).isEqualTo("${customerIdVar}"); }); diff --git a/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.bpmn b/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.bpmn new file mode 100644 index 00000000000..8188ad94c32 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.bpmn @@ -0,0 +1,16 @@ + + + + + + + + orderShipped + + + + + + diff --git a/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.empty.bpmn b/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.empty.bpmn new file mode 100644 index 00000000000..29c0c4c96d0 --- /dev/null +++ b/modules/flowable-bpmn-converter/src/test/resources/intermediateCatchEventRegistryEvent.empty.bpmn @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CancelEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CancelEventDefinition.java index 74506ab75cf..97fe46bdedd 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CancelEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CancelEventDefinition.java @@ -12,11 +12,24 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class CancelEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.END_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + @Override public CancelEventDefinition clone() { CancelEventDefinition clone = new CancelEventDefinition(); diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CompensateEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CompensateEventDefinition.java index 2e68313986d..f5dd331dad2 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CompensateEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CompensateEventDefinition.java @@ -12,11 +12,24 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class CompensateEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.INTERMEDIATE_THROW_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String activityRef; protected boolean waitForCompletion = true; diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ConditionalEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ConditionalEventDefinition.java index de56059853d..1dbdfa1f065 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ConditionalEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ConditionalEventDefinition.java @@ -12,11 +12,25 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class ConditionalEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String conditionExpression; protected String conditionLanguage; diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CustomBpmnEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CustomBpmnEventDefinition.java new file mode 100644 index 00000000000..5d3c4f69937 --- /dev/null +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/CustomBpmnEventDefinition.java @@ -0,0 +1,23 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.model; + +/** + * Marker for {@link EventDefinition} subclasses that are not part of the BPMN 2.0 specification and are + * therefore serialized inside {@code } rather than as direct children of the event + * element. The Flowable-specific {@code EventRegistryEventDefinition} and {@code VariableListenerEventDefinition} + * are built-in implementors; additional {@link EventDefinition} subclasses should also implement this marker + * so the BPMN writer routes them through the extension-element path and round-trips correctly. + */ +public interface CustomBpmnEventDefinition { +} diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ErrorEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ErrorEventDefinition.java index 6ba3813707c..2da616b299e 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ErrorEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/ErrorEventDefinition.java @@ -12,11 +12,25 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class ErrorEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.END_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String errorCode; protected String errorVariableName; protected Boolean errorVariableTransient; diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EscalationEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EscalationEventDefinition.java index 74698a0a203..91bba2360d5 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EscalationEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EscalationEventDefinition.java @@ -12,11 +12,26 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class EscalationEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.END_EVENT, + EventDefinitionLocation.INTERMEDIATE_THROW_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String escalationCode; public String getEscalationCode() { diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinition.java index 3873576cf95..06c2211f480 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinition.java @@ -12,6 +12,8 @@ */ package org.flowable.bpmn.model; +import java.util.Set; + /** * @author Tijs Rademakers */ @@ -19,4 +21,11 @@ public abstract class EventDefinition extends BaseElement { @Override public abstract EventDefinition clone(); + + /** + * Returns the set of {@link EventDefinitionLocation}s where this {@link EventDefinition} is allowed. + * Consulted by the BPMN process validators (start / event-subprocess / intermediate-catch / boundary) + * to decide whether this event definition is valid in a given event host. + */ + public abstract Set getSupportedLocations(); } diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinitionLocation.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinitionLocation.java new file mode 100644 index 00000000000..d2e808e8895 --- /dev/null +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventDefinitionLocation.java @@ -0,0 +1,39 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.model; + +/** + * Where an {@link EventDefinition} is allowed to appear in a process model. Each {@link EventDefinition} + * subclass declares its supported locations via {@link EventDefinition#getSupportedLocations()}; the BPMN + * process validators consult this set instead of hard-coding the allowed types. + */ +public enum EventDefinitionLocation { + + /** Top-level start event of a {@code Process} (i.e. not inside an {@code EventSubProcess}). */ + START_EVENT, + + /** Start event inside an {@code EventSubProcess}. */ + EVENT_SUBPROCESS_START_EVENT, + + /** {@code IntermediateCatchEvent} body. */ + INTERMEDIATE_CATCH_EVENT, + + /** {@code BoundaryEvent} attached to an activity. */ + BOUNDARY_EVENT, + + /** {@code EndEvent} body — covers {@code Terminate}, {@code Cancel}, {@code Error}, {@code Escalation} end events. */ + END_EVENT, + + /** {@code IntermediateThrowEvent} body — covers {@code Compensate}, {@code Escalation}, {@code Signal} throw events. */ + INTERMEDIATE_THROW_EVENT +} diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventRegistryEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventRegistryEventDefinition.java new file mode 100644 index 00000000000..d30241973e7 --- /dev/null +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/EventRegistryEventDefinition.java @@ -0,0 +1,60 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.model; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +public class EventRegistryEventDefinition extends EventDefinition implements CustomBpmnEventDefinition { + + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.START_EVENT, + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + + protected String eventDefinitionKey; + + public EventRegistryEventDefinition() { + } + + public EventRegistryEventDefinition(String eventDefinitionKey) { + this.eventDefinitionKey = eventDefinitionKey; + } + + public String getEventDefinitionKey() { + return eventDefinitionKey; + } + + public void setEventDefinitionKey(String eventDefinitionKey) { + this.eventDefinitionKey = eventDefinitionKey; + } + + @Override + public EventRegistryEventDefinition clone() { + EventRegistryEventDefinition clone = new EventRegistryEventDefinition(); + clone.setValues(this); + return clone; + } + + public void setValues(EventRegistryEventDefinition otherDefinition) { + super.setValues(otherDefinition); + setEventDefinitionKey(otherDefinition.getEventDefinitionKey()); + } +} diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/MessageEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/MessageEventDefinition.java index 248213b0888..222312f214a 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/MessageEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/MessageEventDefinition.java @@ -12,14 +12,29 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class MessageEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.START_EVENT, + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT)); + protected String messageRef; protected String messageExpression; + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + public String getMessageRef() { return messageRef; } diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/SignalEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/SignalEventDefinition.java index 7881903ff9a..32b2de61063 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/SignalEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/SignalEventDefinition.java @@ -12,11 +12,27 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class SignalEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.START_EVENT, + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.INTERMEDIATE_THROW_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String signalRef; protected String signalExpression; protected boolean async; diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TerminateEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TerminateEventDefinition.java index 69c3d75cdbf..c4902830552 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TerminateEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TerminateEventDefinition.java @@ -12,12 +12,23 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers * @author Joram Barrez */ public class TerminateEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of(EventDefinitionLocation.END_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + /** * When true, this event will terminate all parent process instances (in the case of using call activity), thus ending the whole process instance. * diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TimerEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TimerEventDefinition.java index 5ba55815908..0374e301f57 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TimerEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/TimerEventDefinition.java @@ -12,11 +12,26 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ public class TimerEventDefinition extends EventDefinition { + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.START_EVENT, + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + protected String timeDate; protected String timeDuration; protected String timeCycle; diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/VariableListenerEventDefinition.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/VariableListenerEventDefinition.java index dd86256090d..a68c6be6fe0 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/VariableListenerEventDefinition.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/VariableListenerEventDefinition.java @@ -12,10 +12,24 @@ */ package org.flowable.bpmn.model; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + /** * @author Tijs Rademakers */ -public class VariableListenerEventDefinition extends EventDefinition { +public class VariableListenerEventDefinition extends EventDefinition implements CustomBpmnEventDefinition { + + private static final Set SUPPORTED_LOCATIONS = Collections.unmodifiableSet(EnumSet.of( + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT)); + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } public static final String CHANGE_TYPE_ALL = "all"; public static final String CHANGE_TYPE_UPDATE = "update"; diff --git a/modules/flowable-bpmn-model/src/test/java/org/flowable/bpmn/model/EventDefinitionSupportedLocationsTest.java b/modules/flowable-bpmn-model/src/test/java/org/flowable/bpmn/model/EventDefinitionSupportedLocationsTest.java new file mode 100644 index 00000000000..c18f66c7a7d --- /dev/null +++ b/modules/flowable-bpmn-model/src/test/java/org/flowable/bpmn/model/EventDefinitionSupportedLocationsTest.java @@ -0,0 +1,89 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.bpmn.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.flowable.bpmn.model.EventDefinitionLocation.BOUNDARY_EVENT; +import static org.flowable.bpmn.model.EventDefinitionLocation.END_EVENT; +import static org.flowable.bpmn.model.EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT; +import static org.flowable.bpmn.model.EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT; +import static org.flowable.bpmn.model.EventDefinitionLocation.INTERMEDIATE_THROW_EVENT; +import static org.flowable.bpmn.model.EventDefinitionLocation.START_EVENT; + +import java.util.EnumSet; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class EventDefinitionSupportedLocationsTest { + + static Stream supportedLocationsPerSubtype() { + return Stream.of( + Arguments.of(supplier(CancelEventDefinition::new), + EnumSet.of(BOUNDARY_EVENT, END_EVENT)), + Arguments.of(supplier(CompensateEventDefinition::new), + EnumSet.of(BOUNDARY_EVENT, INTERMEDIATE_THROW_EVENT)), + Arguments.of(supplier(ConditionalEventDefinition::new), + EnumSet.of(EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT)), + Arguments.of(supplier(ErrorEventDefinition::new), + EnumSet.of(EVENT_SUBPROCESS_START_EVENT, BOUNDARY_EVENT, END_EVENT)), + Arguments.of(supplier(EscalationEventDefinition::new), + EnumSet.of(EVENT_SUBPROCESS_START_EVENT, BOUNDARY_EVENT, END_EVENT, INTERMEDIATE_THROW_EVENT)), + Arguments.of(supplier(EventRegistryEventDefinition::new), + EnumSet.of(START_EVENT, EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT)), + Arguments.of(supplier(MessageEventDefinition::new), + EnumSet.of(START_EVENT, EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT)), + Arguments.of(supplier(SignalEventDefinition::new), + EnumSet.of(START_EVENT, EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT, INTERMEDIATE_THROW_EVENT)), + Arguments.of(supplier(TerminateEventDefinition::new), + EnumSet.of(END_EVENT)), + Arguments.of(supplier(TimerEventDefinition::new), + EnumSet.of(START_EVENT, EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT)), + Arguments.of(supplier(VariableListenerEventDefinition::new), + EnumSet.of(EVENT_SUBPROCESS_START_EVENT, INTERMEDIATE_CATCH_EVENT, BOUNDARY_EVENT))); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("supportedLocationsPerSubtype") + void declaresExpectedSupportedLocations(Supplier factory, Set expected) { + EventDefinition definition = factory.get(); + assertThat(definition.getSupportedLocations()).containsExactlyInAnyOrderElementsOf(expected); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("supportedLocationsPerSubtype") + void supportedLocationsIsImmutable(Supplier factory) { + Set locations = factory.get().getSupportedLocations(); + assertThat(locations).isUnmodifiable(); + } + + private static Supplier supplier(Supplier factory) { + Class type = factory.get().getClass(); + return new Supplier<>() { + + @Override + public EventDefinition get() { + return factory.get(); + } + + @Override + public String toString() { + return type.getSimpleName(); + } + }; + } +} diff --git a/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/CustomEventListenerXmlFactory.java b/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/CustomEventListenerXmlFactory.java new file mode 100644 index 00000000000..4b146ac9a8d --- /dev/null +++ b/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/CustomEventListenerXmlFactory.java @@ -0,0 +1,32 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.converter; + +import javax.xml.stream.XMLStreamReader; + +import org.flowable.cmmn.model.EventListener; + +/** + * Builds an {@link EventListener} model object from an XML {@code } element whose + * {@code flowable:eventType} attribute matches a key registered via + * {@link GenericEventListenerXmlConverter#addCustomListenerTypeFactory}. + *

+ * Common attributes (name, available-condition) are populated by {@code convertCommonAttributes} after the + * factory returns; the {@code id} is set by {@code BaseCmmnXmlConverter.convertToCmmnModel}. The factory only + * needs to read attributes specific to its custom listener type. + */ +@FunctionalInterface +public interface CustomEventListenerXmlFactory { + + EventListener create(XMLStreamReader xtr); +} diff --git a/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/GenericEventListenerXmlConverter.java b/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/GenericEventListenerXmlConverter.java index cfa6109cd85..fc750517f77 100644 --- a/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/GenericEventListenerXmlConverter.java +++ b/modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/GenericEventListenerXmlConverter.java @@ -12,8 +12,13 @@ */ package org.flowable.cmmn.converter; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + import javax.xml.stream.XMLStreamReader; +import org.apache.commons.lang3.StringUtils; import org.flowable.cmmn.model.BaseElement; import org.flowable.cmmn.model.EventListener; import org.flowable.cmmn.model.GenericEventListener; @@ -21,12 +26,62 @@ import org.flowable.cmmn.model.ReactivateEventListener; import org.flowable.cmmn.model.SignalEventListener; import org.flowable.cmmn.model.VariableEventListener; +import org.flowable.common.engine.api.FlowableIllegalArgumentException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Tijs Rademakers */ public class GenericEventListenerXmlConverter extends PlanItemDefinitionXmlConverter { + private static final Logger LOGGER = LoggerFactory.getLogger(GenericEventListenerXmlConverter.class); + + /** + * Registry of {@code flowable:eventListenerType} discriminator values to factories that build the typed + * {@link EventListener} model object. Pre-populated with the four built-in discriminators ("signal", + * "variable", "intent", "reactivate"); additional types are added via + * {@link #addCustomListenerTypeFactory(String, CustomEventListenerXmlFactory)}. + *

+ * Lifecycle: JVM-wide static, shared by every {@link CmmnXmlConverter} instance in the process. + * Registration is last-writer-wins for added types; the four built-in keys are reserved and cannot be + * overridden. Tests that build multiple engines should call + * {@link #removeCustomListenerTypeFactory(String)} between runs to keep the registry clean. + */ + private static final Map LISTENER_TYPE_FACTORIES = new HashMap<>(); + + private static final Set BUILT_IN_LISTENER_TYPES = Set.of("signal", "variable", "intent", "reactivate"); + + static { + LISTENER_TYPE_FACTORIES.put("signal", xtr -> { + SignalEventListener listener = new SignalEventListener(); + listener.setSignalRef(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_SIGNAL_REF)); + return listener; + }); + LISTENER_TYPE_FACTORIES.put("variable", xtr -> { + VariableEventListener listener = new VariableEventListener(); + listener.setVariableName(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_VARIABLE_NAME)); + listener.setVariableChangeType(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_VARIABLE_CHANGE_TYPE)); + return listener; + }); + LISTENER_TYPE_FACTORIES.put("intent", xtr -> new IntentEventListener()); + LISTENER_TYPE_FACTORIES.put("reactivate", xtr -> new ReactivateEventListener()); + } + + public static void addCustomListenerTypeFactory(String listenerType, CustomEventListenerXmlFactory factory) { + if (BUILT_IN_LISTENER_TYPES.contains(listenerType)) { + throw new FlowableIllegalArgumentException("Cannot override built-in eventListenerType '" + listenerType + "'"); + } + LISTENER_TYPE_FACTORIES.put(listenerType, factory); + } + + public static void removeCustomListenerTypeFactory(String listenerType) { + if (BUILT_IN_LISTENER_TYPES.contains(listenerType)) { + return; + } + LISTENER_TYPE_FACTORIES.remove(listenerType); + } + @Override public boolean hasChildElements() { return true; @@ -40,29 +95,20 @@ public String getXMLElementName() { @Override protected BaseElement convert(XMLStreamReader xtr, ConversionHelper conversionHelper) { String listenerType = xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_TYPE); - if ("signal".equals(listenerType)) { - SignalEventListener signalEventListener = new SignalEventListener(); - signalEventListener.setSignalRef(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_SIGNAL_REF)); - return convertCommonAttributes(xtr, signalEventListener); - - } else if ("variable".equals(listenerType)) { - VariableEventListener variableEventListener = new VariableEventListener(); - variableEventListener.setVariableName(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_VARIABLE_NAME)); - variableEventListener.setVariableChangeType(xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_VARIABLE_CHANGE_TYPE)); - return convertCommonAttributes(xtr, variableEventListener); - - } else if ("intent".equals(listenerType)) { - IntentEventListener intentEventListener = new IntentEventListener(); - return convertCommonAttributes(xtr, intentEventListener); - - } else if ("reactivate".equals(listenerType)) { - ReactivateEventListener reactivateEventListener = new ReactivateEventListener(); - return convertCommonAttributes(xtr, reactivateEventListener); - - } else { - GenericEventListener genericEventListener = new GenericEventListener(); - return convertCommonAttributes(xtr, genericEventListener); + CustomEventListenerXmlFactory factory = listenerType != null ? LISTENER_TYPE_FACTORIES.get(listenerType) : null; + if (factory != null) { + return convertCommonAttributes(xtr, factory.create(xtr)); } + + if (StringUtils.isNotEmpty(listenerType)) { + // listenerType is set but matches no registered factory — likely a typo or missing + // addCustomEventListenerTypeFactory registration. Falling through to GenericEventListener would + // silently produce a non-firing listener; warn so the misconfiguration surfaces at parse time. + LOGGER.warn("Unrecognized flowable:eventType '{}' on eventListener '{}'; falling back to a generic event listener. Registered types: {}", + listenerType, xtr.getAttributeValue(null, CmmnXmlConstants.ATTRIBUTE_ID), LISTENER_TYPE_FACTORIES.keySet()); + } + + return convertCommonAttributes(xtr, new GenericEventListener()); } protected EventListener convertCommonAttributes(XMLStreamReader xtr, EventListener listener) { @@ -71,4 +117,5 @@ protected EventListener convertCommonAttributes(XMLStreamReader xtr, EventListen CmmnXmlConstants.ATTRIBUTE_EVENT_LISTENER_AVAILABLE_CONDITION)); return listener; } + } diff --git a/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/eventListenerUnrecognizedType.cmmn b/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/eventListenerUnrecognizedType.cmmn new file mode 100644 index 00000000000..b8033975bfa --- /dev/null +++ b/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/eventListenerUnrecognizedType.cmmn @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java index 85e596cb17e..0549a8ba682 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java @@ -52,6 +52,8 @@ import org.flowable.cmmn.api.listener.CaseInstanceLifecycleListener; import org.flowable.cmmn.api.listener.PlanItemInstanceLifecycleListener; import org.flowable.cmmn.api.migration.CaseInstanceMigrationCallback; +import org.flowable.cmmn.converter.CustomEventListenerXmlFactory; +import org.flowable.cmmn.converter.GenericEventListenerXmlConverter; import org.flowable.cmmn.engine.impl.CmmnEngineImpl; import org.flowable.cmmn.engine.impl.CmmnEnginePostEngineBuildConsumer; import org.flowable.cmmn.engine.impl.CmmnHistoryServiceImpl; @@ -123,6 +125,7 @@ import org.flowable.cmmn.engine.impl.parser.DefaultCmmnActivityBehaviorFactory; import org.flowable.cmmn.engine.impl.parser.handler.CasePageTaskParseHandler; import org.flowable.cmmn.engine.impl.parser.handler.CaseParseHandler; +import org.flowable.cmmn.engine.impl.parser.handler.EventRegistryCaseStartLifecycleParseHandler; import org.flowable.cmmn.engine.impl.parser.handler.CaseTaskParseHandler; import org.flowable.cmmn.engine.impl.parser.handler.DecisionTaskParseHandler; import org.flowable.cmmn.engine.impl.parser.handler.ExternalWorkerServiceTaskParseHandler; @@ -424,6 +427,12 @@ public class CmmnEngineConfiguration extends AbstractBuildableEngineConfiguratio protected List preCmmnParseHandlers; protected List postCmmnParseHandlers; protected List customCmmnParseHandlers; + /** + * Factories that build custom {@link org.flowable.cmmn.model.EventListener} model objects from XML + * {@code } elements with custom {@code flowable:eventListenerType} discriminator values. + * Registered with {@link GenericEventListenerXmlConverter#addCustomListenerTypeFactory} during engine init. + */ + protected Map customEventListenerTypeFactories; protected CmmnListenerFactory listenerFactory; protected CmmnListenerNotificationHelper listenerNotificationHelper; @@ -1165,6 +1174,12 @@ protected void initDeploymentManager() { } public void initCmmnParser() { + if (customEventListenerTypeFactories != null) { + for (Map.Entry entry : customEventListenerTypeFactories.entrySet()) { + GenericEventListenerXmlConverter.addCustomListenerTypeFactory(entry.getKey(), entry.getValue()); + } + } + if (cmmnParser == null) { CmmnParserImpl cmmnParserImpl = new CmmnParserImpl(); cmmnParserImpl.setActivityBehaviorFactory(activityBehaviorFactory); @@ -1187,6 +1202,7 @@ public void initCmmnParser() { public List getDefaultCmmnParseHandlers() { List cmmnParseHandlers = new ArrayList<>(); cmmnParseHandlers.add(new CaseParseHandler()); + cmmnParseHandlers.add(new EventRegistryCaseStartLifecycleParseHandler()); cmmnParseHandlers.add(new CaseTaskParseHandler()); cmmnParseHandlers.add(new DecisionTaskParseHandler()); cmmnParseHandlers.add(new HumanTaskParseHandler()); @@ -2405,6 +2421,23 @@ public CmmnEngineConfiguration setCustomCmmnParseHandlers(List return this; } + public Map getCustomEventListenerTypeFactories() { + return customEventListenerTypeFactories; + } + + public CmmnEngineConfiguration setCustomEventListenerTypeFactories(Map customEventListenerTypeFactories) { + this.customEventListenerTypeFactories = customEventListenerTypeFactories; + return this; + } + + public CmmnEngineConfiguration addCustomEventListenerTypeFactory(String listenerType, CustomEventListenerXmlFactory factory) { + if (customEventListenerTypeFactories == null) { + customEventListenerTypeFactories = new HashMap<>(); + } + customEventListenerTypeFactories.put(listenerType, factory); + return this; + } + public CmmnListenerFactory getListenerFactory() { return listenerFactory; } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartDeployContext.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartDeployContext.java new file mode 100644 index 00000000000..bed86a90765 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartDeployContext.java @@ -0,0 +1,78 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.deployer; + +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity; +import org.flowable.cmmn.model.Case; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.eventsubscription.service.EventSubscriptionService; + +/** + * Context passed to {@link CaseDefinitionStartLifecycleHandler#deploy} carrying the freshly-deployed + * case definition together with its parsed {@link Case} model. + */ +public class CaseDefinitionStartDeployContext { + + protected final CaseDefinitionEntity caseDefinition; + protected final Case caseModel; + protected final CmmnEngineConfiguration cmmnEngineConfiguration; + protected final CommandContext commandContext; + protected final boolean restoringPreviousVersion; + + public CaseDefinitionStartDeployContext(CaseDefinitionEntity caseDefinition, Case caseModel, + CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext) { + this(caseDefinition, caseModel, cmmnEngineConfiguration, commandContext, false); + } + + public CaseDefinitionStartDeployContext(CaseDefinitionEntity caseDefinition, Case caseModel, + CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext, + boolean restoringPreviousVersion) { + this.caseDefinition = caseDefinition; + this.caseModel = caseModel; + this.cmmnEngineConfiguration = cmmnEngineConfiguration; + this.commandContext = commandContext; + this.restoringPreviousVersion = restoringPreviousVersion; + } + + public CaseDefinitionEntity getCaseDefinition() { + return caseDefinition; + } + + public Case getCaseModel() { + return caseModel; + } + + public CmmnEngineConfiguration getCmmnEngineConfiguration() { + return cmmnEngineConfiguration; + } + + public CommandContext getCommandContext() { + return commandContext; + } + + public EventSubscriptionService getEventSubscriptionService() { + return cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); + } + + /** + * {@code true} when this deploy is restoring a previous (earlier-version) case definition's start + * triggers because the latest version's deployment was just deleted. Behaviors should skip duplicate- + * subscription validation in this mode — the just-deleted case definition's subscriptions may still + * be in the in-session entity cache (the bulk delete hasn't been flushed yet) so a re-insert would + * otherwise trip a false-positive conflict. The fresh-deployment path leaves this {@code false}. + */ + public boolean isRestoringPreviousVersion() { + return restoringPreviousVersion; + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartLifecycleHandler.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartLifecycleHandler.java new file mode 100644 index 00000000000..9d32d876fbf --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartLifecycleHandler.java @@ -0,0 +1,47 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.deployer; + +/** + * Implemented by an opt-in handler attached to a {@code Case} that owns a deploy-time start trigger + * (event-registry subscription, timer cron, custom subscription type, etc.). The + * {@link CmmnDeployer} iterates the handlers attached to each case via + * {@code Case.getStartLifecycleHandlers()} and calls {@link #deploy} on the freshly-deployed case + * definition's handlers / {@link #undeploy} on the previous (now-superseded) case definition's + * handlers. + *

+ * Restoration after deployment-deletion goes through {@link #deploy} too — the deploy context's + * {@code isRestoringPreviousVersion()} flag distinguishes it from a fresh deploy. Both methods must + * be implemented; a handler that opts into the deploy-time lifecycle owns both halves. + *

+ * Custom integrations install additional handlers via a custom {@code CmmnParseHandler} (registered + * in {@code customCmmnParseHandlers}) that calls {@code case.addStartLifecycleHandler(...)} during + * parsing. Multiple handlers may co-exist on a single case. + */ +public interface CaseDefinitionStartLifecycleHandler { + + /** + * Register the deploy-time artifact (event subscription, timer job, etc.) for this case + * definition's start trigger when it is freshly deployed. Also called via the + * deployment-deletion restoration path when the previous version's start triggers are + * restored — distinguished by {@link CaseDefinitionStartDeployContext#isRestoringPreviousVersion()}. + */ + void deploy(CaseDefinitionStartDeployContext context); + + /** + * Remove or update the deploy-time artifact for this case definition's start trigger when + * its case definition is superseded by a new version. Called on the previous (now-superseded) + * case definition's handlers. + */ + void undeploy(CaseDefinitionStartUndeployContext context); +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartUndeployContext.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartUndeployContext.java new file mode 100644 index 00000000000..f951095fecd --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CaseDefinitionStartUndeployContext.java @@ -0,0 +1,87 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.deployer; + +import java.util.Set; + +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity; +import org.flowable.cmmn.model.Case; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.eventsubscription.service.EventSubscriptionService; + +/** + * Context passed to {@link CaseDefinitionStartLifecycleHandler#undeploy} when a case definition is + * being superseded by a new version. Carries both the previous (now-superseded) case definition and + * the new one — most handlers only need the previous case definition, but the EventRegistry "manual" + * re-point branch updates subscriptions to point at {@link #getNewCaseDefinition()}. + *

+ * Handlers register their obsolete event subscription types via + * {@link #registerObsoleteEventSubscriptionType(String)}. The deployer issues one mass-delete per + * unique registered type after the undeploy iteration — fewer DB round-trips than per-handler + * deletes, and tighter than a fixed sweep that always ran regardless of which types the previous + * case definition actually used. + */ +public class CaseDefinitionStartUndeployContext { + + protected final CaseDefinitionEntity previousCaseDefinition; + protected final CaseDefinitionEntity newCaseDefinition; + protected final Case previousCaseModel; + protected final CmmnEngineConfiguration cmmnEngineConfiguration; + protected final CommandContext commandContext; + protected final Set obsoleteEventSubscriptionTypes; + + public CaseDefinitionStartUndeployContext(CaseDefinitionEntity previousCaseDefinition, CaseDefinitionEntity newCaseDefinition, + Case previousCaseModel, CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext, + Set obsoleteEventSubscriptionTypes) { + this.previousCaseDefinition = previousCaseDefinition; + this.newCaseDefinition = newCaseDefinition; + this.previousCaseModel = previousCaseModel; + this.cmmnEngineConfiguration = cmmnEngineConfiguration; + this.commandContext = commandContext; + this.obsoleteEventSubscriptionTypes = obsoleteEventSubscriptionTypes; + } + + public CaseDefinitionEntity getPreviousCaseDefinition() { + return previousCaseDefinition; + } + + public CaseDefinitionEntity getNewCaseDefinition() { + return newCaseDefinition; + } + + public Case getPreviousCaseModel() { + return previousCaseModel; + } + + public CmmnEngineConfiguration getCmmnEngineConfiguration() { + return cmmnEngineConfiguration; + } + + public CommandContext getCommandContext() { + return commandContext; + } + + public EventSubscriptionService getEventSubscriptionService() { + return cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); + } + + /** + * Registers an event subscription event-type that the deployer should mass-delete for the previous + * case definition (scope-type CMMN, scope-id null) after the undeploy iteration. Multiple handlers + * registering the same type result in a single DB sweep. + */ + public void registerObsoleteEventSubscriptionType(String eventType) { + obsoleteEventSubscriptionTypes.add(eventType); + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java index cbce439b1e2..6ab29ae7fb7 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java @@ -13,16 +13,13 @@ package org.flowable.cmmn.engine.impl.deployer; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.flowable.cmmn.converter.CmmnXmlConstants; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.parser.CmmnParseContext; import org.flowable.cmmn.engine.impl.parser.CmmnParseResult; @@ -32,11 +29,9 @@ import org.flowable.cmmn.engine.impl.persistence.entity.CmmnDeploymentEntity; import org.flowable.cmmn.engine.impl.persistence.entity.CmmnResourceEntity; import org.flowable.cmmn.engine.impl.persistence.entity.deploy.CaseDefinitionCacheEntry; -import org.flowable.cmmn.engine.impl.util.CmmnCorrelationUtil; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.cmmn.model.Case; import org.flowable.cmmn.model.CmmnModel; -import org.flowable.cmmn.model.ExtensionElement; import org.flowable.cmmn.validation.CaseValidator; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.delegate.Expression; @@ -46,9 +41,12 @@ import org.flowable.common.engine.impl.EngineDeployer; import org.flowable.common.engine.impl.assignment.CandidateUtil; import org.flowable.common.engine.impl.cfg.IdGenerator; +import org.flowable.common.engine.impl.context.Context; import org.flowable.common.engine.impl.el.ExpressionManager; +import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; import org.flowable.eventsubscription.service.EventSubscriptionService; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; import org.flowable.identitylink.api.IdentityLinkType; import org.flowable.identitylink.service.IdentityLinkService; import org.flowable.identitylink.service.impl.persistence.entity.IdentityLinkEntity; @@ -193,39 +191,45 @@ protected void persistCaseDefinitions(CmmnParseResult parseResult) { protected void updateEventSubscriptions(CmmnParseResult parseResult, Map mapOfNewCaseDefinitionToPreviousVersion) { EventSubscriptionService eventSubscriptionService = cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) { + CommandContext commandContext = Context.getCommandContext(); + for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) { + Case newCaseModel = parseResult.getCmmnCaseForCaseDefinition(caseDefinition); CaseDefinitionEntity previousCaseDefinition = mapOfNewCaseDefinitionToPreviousVersion.get(caseDefinition); + if (previousCaseDefinition != null) { - if (isManualCorrelationSubscriptionConfiguration(parseResult, previousCaseDefinition)) { - // for a dynamic event registry start event, we don't remove the manually registered subscriptions, but rather update them to the newest - // case definition, if required - String startEventType = getCaseModel(parseResult, previousCaseDefinition).getPrimaryCase().getStartEventType(); - updateOldEventSubscriptions(previousCaseDefinition, caseDefinition, startEventType); - } else { - // for a static event registry start event, we delete the old subscription and will later create a new one - eventSubscriptionService.deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(previousCaseDefinition.getId(), ScopeTypes.CMMN); + Case previousCaseModel = getCaseModel(parseResult, previousCaseDefinition).getPrimaryCase(); + Set obsoleteEventTypes = new LinkedHashSet<>(); + + for (Object handler : previousCaseModel.getStartLifecycleHandlers()) { + if (handler instanceof CaseDefinitionStartLifecycleHandler lifecycleHandler) { + lifecycleHandler.undeploy(new CaseDefinitionStartUndeployContext( + previousCaseDefinition, caseDefinition, previousCaseModel, + cmmnEngineConfiguration, commandContext, obsoleteEventTypes)); + } + } + + if (!obsoleteEventTypes.isEmpty()) { + deleteObsoleteEventSubscriptions(previousCaseDefinition, obsoleteEventTypes, eventSubscriptionService); } } - // create new subscriptions, but only for static event registry start events - Case caseModel = parseResult.getCmmnCaseForCaseDefinition(caseDefinition); - String startEventType = caseModel.getStartEventType(); - if (startEventType != null && !isManualCorrelationSubscriptionConfiguration(parseResult, caseDefinition)) { - eventSubscriptionService.createEventSubscriptionBuilder() - .eventType(startEventType) - .configuration(getEventCorrelationKey(caseModel)) - .scopeDefinitionId(caseDefinition.getId()) - .scopeType(ScopeTypes.CMMN) - .tenantId(caseDefinition.getTenantId()) - .create(); + for (Object handler : newCaseModel.getStartLifecycleHandlers()) { + if (handler instanceof CaseDefinitionStartLifecycleHandler lifecycleHandler) { + lifecycleHandler.deploy(new CaseDefinitionStartDeployContext( + caseDefinition, newCaseModel, cmmnEngineConfiguration, commandContext)); + } } } } - protected void updateOldEventSubscriptions(CaseDefinitionEntity previousCaseDefinition, CaseDefinitionEntity caseDefinition, String eventType) { - CommandContextUtil.getCmmnEngineConfiguration().getEventSubscriptionServiceConfiguration().getEventSubscriptionService().updateEventSubscriptionScopeDefinitionId( - previousCaseDefinition.getId(), caseDefinition.getId(), eventType, caseDefinition.getKey(), null); + protected void deleteObsoleteEventSubscriptions(CaseDefinitionEntity previousCaseDefinition, Collection eventTypes, + EventSubscriptionService eventSubscriptionService) { + List subscriptionsToDelete = eventSubscriptionService.findEventSubscriptionsByTypesAndScopeDefinitionId( + eventTypes, previousCaseDefinition.getId(), ScopeTypes.CMMN, previousCaseDefinition.getTenantId()); + for (EventSubscriptionEntity subscription : subscriptionsToDelete) { + eventSubscriptionService.deleteEventSubscription(subscription); + } } protected CmmnModel getCaseModel(CmmnParseResult parseResult, CaseDefinitionEntity caseDefinition) { @@ -237,20 +241,6 @@ protected CmmnModel getCaseModel(CmmnParseResult parseResult, CaseDefinitionEnti return caseModel; } - protected boolean isManualCorrelationSubscriptionConfiguration(CmmnParseResult parseResult, CaseDefinitionEntity caseDefinition) { - CmmnModel caseModel = getCaseModel(parseResult, caseDefinition); - List correlationCfgExtensions = caseModel.getPrimaryCase().getExtensionElements() - .getOrDefault(CmmnXmlConstants.START_EVENT_CORRELATION_CONFIGURATION, Collections.emptyList()); - if (!correlationCfgExtensions.isEmpty()) { - return Objects.equals(correlationCfgExtensions.get(0).getElementText(), CmmnXmlConstants.START_EVENT_CORRELATION_MANUAL); - } - return false; - } - - protected String getEventCorrelationKey(Case caseModel) { - return CmmnCorrelationUtil.getCorrelationKey(CmmnXmlConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, CommandContextUtil.getCommandContext(), caseModel); - } - protected void makeCaseDefinitionsConsistentWithPersistedVersions(CmmnParseResult parseResult) { for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) { CaseDefinitionEntity persistedCaseDefinition = getPersistedInstanceOfCaseDefinition(caseDefinition); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/EventRegistryCaseDefinitionStartLifecycleHandler.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/EventRegistryCaseDefinitionStartLifecycleHandler.java new file mode 100644 index 00000000000..426e1e0eafb --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/EventRegistryCaseDefinitionStartLifecycleHandler.java @@ -0,0 +1,87 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.deployer; + +import org.flowable.cmmn.converter.CmmnXmlConstants; +import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity; +import org.flowable.cmmn.engine.impl.util.CmmnCorrelationUtil; +import org.flowable.cmmn.model.Case; +import org.flowable.common.engine.api.scope.ScopeTypes; +import org.flowable.eventsubscription.api.EventSubscriptionBuilder; +import org.flowable.eventsubscription.service.EventSubscriptionService; + +/** + * Built-in case-start lifecycle handler for the BPMN-spec-extension event-registry case start. Owns + * the deploy-time event-subscription registration that historically lived inline in + * {@link CmmnDeployer#updateEventSubscriptions} and + * {@code CmmnDeploymentEntityManagerImpl.restoreEventRegistryStartEvent}. + */ +public class EventRegistryCaseDefinitionStartLifecycleHandler implements CaseDefinitionStartLifecycleHandler { + + protected final String eventType; + protected final boolean manualCorrelation; + + public EventRegistryCaseDefinitionStartLifecycleHandler(String eventType, boolean manualCorrelation) { + this.eventType = eventType; + this.manualCorrelation = manualCorrelation; + } + + @Override + public void deploy(CaseDefinitionStartDeployContext context) { + // Manual-correlation mode: subscriptions are added explicitly by the application at runtime, + // not at deploy time. + if (manualCorrelation) { + return; + } + + CaseDefinitionEntity caseDefinition = context.getCaseDefinition(); + Case caseModel = context.getCaseModel(); + EventSubscriptionService eventSubscriptionService = context.getEventSubscriptionService(); + + EventSubscriptionBuilder builder = eventSubscriptionService.createEventSubscriptionBuilder() + .eventType(eventType) + .configuration(CmmnCorrelationUtil.getCorrelationKey(CmmnXmlConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, + context.getCommandContext(), caseModel)) + .scopeDefinitionId(caseDefinition.getId()) + .scopeType(ScopeTypes.CMMN); + + if (caseDefinition.getTenantId() != null) { + builder.tenantId(caseDefinition.getTenantId()); + } + + builder.create(); + } + + @Override + public void undeploy(CaseDefinitionStartUndeployContext context) { + if (manualCorrelation) { + // dynamic mode: keep existing subscriptions but re-point them to the new case definition + // instead of deleting. + CaseDefinitionEntity previousCaseDefinition = context.getPreviousCaseDefinition(); + CaseDefinitionEntity newCaseDefinition = context.getNewCaseDefinition(); + context.getEventSubscriptionService().updateEventSubscriptionScopeDefinitionId( + previousCaseDefinition.getId(), newCaseDefinition.getId(), + eventType, newCaseDefinition.getKey(), null); + } else { + context.registerObsoleteEventSubscriptionType(eventType); + } + } + + public String getEventType() { + return eventType; + } + + public boolean isManualCorrelation() { + return manualCorrelation; + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/parser/handler/EventRegistryCaseStartLifecycleParseHandler.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/parser/handler/EventRegistryCaseStartLifecycleParseHandler.java new file mode 100644 index 00000000000..a01e475e963 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/parser/handler/EventRegistryCaseStartLifecycleParseHandler.java @@ -0,0 +1,59 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.parser.handler; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.flowable.cmmn.converter.CmmnXmlConstants; +import org.flowable.cmmn.engine.impl.deployer.EventRegistryCaseDefinitionStartLifecycleHandler; +import org.flowable.cmmn.engine.impl.parser.CmmnParseResult; +import org.flowable.cmmn.engine.impl.parser.CmmnParserImpl; +import org.flowable.cmmn.model.BaseElement; +import org.flowable.cmmn.model.Case; +import org.flowable.cmmn.model.ExtensionElement; + +/** + * Installs the built-in event-registry case-start lifecycle handler on a {@link Case} when its + * {@code startEventType} is set. Resolves the manual-correlation flag from the case's extension + * elements at parse time so the handler carries it as a constructor arg. + *

+ * Custom integrations can install additional {@code CaseDefinitionStartLifecycleHandler}s on the + * same case via their own parse handlers registered through {@code customCmmnParseHandlers}. + */ +public class EventRegistryCaseStartLifecycleParseHandler extends AbstractCmmnParseHandler { + + @Override + public Collection> getHandledTypes() { + return Collections.singletonList(Case.class); + } + + @Override + protected void executeParse(CmmnParserImpl cmmnParser, CmmnParseResult cmmnParseResult, Case caze) { + String startEventType = caze.getStartEventType(); + if (startEventType == null) { + return; + } + boolean manualCorrelation = isManualCorrelation(caze); + caze.addStartLifecycleHandler(new EventRegistryCaseDefinitionStartLifecycleHandler(startEventType, manualCorrelation)); + } + + protected boolean isManualCorrelation(Case caze) { + List correlationConfig = caze.getExtensionElements() + .getOrDefault(CmmnXmlConstants.START_EVENT_CORRELATION_CONFIGURATION, Collections.emptyList()); + return !correlationConfig.isEmpty() + && Objects.equals(correlationConfig.get(0).getElementText(), CmmnXmlConstants.START_EVENT_CORRELATION_MANUAL); + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/CmmnDeploymentEntityManagerImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/CmmnDeploymentEntityManagerImpl.java index 88e63e6a1c7..f8c3558ff2c 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/CmmnDeploymentEntityManagerImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/CmmnDeploymentEntityManagerImpl.java @@ -19,17 +19,18 @@ import org.flowable.cmmn.api.repository.CaseDefinitionQuery; import org.flowable.cmmn.api.repository.CmmnDeployment; import org.flowable.cmmn.api.repository.CmmnDeploymentQuery; -import org.flowable.cmmn.converter.CmmnXmlConstants; import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionStartDeployContext; +import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionStartLifecycleHandler; import org.flowable.cmmn.engine.impl.persistence.entity.data.CmmnDeploymentDataManager; import org.flowable.cmmn.engine.impl.repository.CaseDefinitionUtil; import org.flowable.cmmn.engine.impl.repository.CmmnDeploymentQueryImpl; -import org.flowable.cmmn.engine.impl.util.CmmnCorrelationUtil; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.cmmn.model.Case; import org.flowable.cmmn.model.CmmnModel; import org.flowable.common.engine.api.repository.EngineResource; import org.flowable.common.engine.api.scope.ScopeTypes; +import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.common.engine.impl.persistence.entity.AbstractEngineEntityManager; /** @@ -80,31 +81,31 @@ public void deleteDeploymentAndRelatedData(String deploymentId, boolean cascade) protected void restorePreviousStartEventsIfNeeded(CaseDefinition caseDefinition) { CaseDefinitionEntity latestCaseDefinition = findLatestCaseDefinition(caseDefinition); - if (latestCaseDefinition != null && caseDefinition.getId().equals(latestCaseDefinition.getId())) { - - // Try to find a previous version (it could be some versions are missing due to deletions) - CaseDefinition previousCaseDefinition = findNewLatestCaseDefinitionAfterRemovalOf(caseDefinition); - if (previousCaseDefinition != null) { - CmmnModel cmmnModel = CaseDefinitionUtil.getCmmnModel(caseDefinition.getId()); - Case caseModel = cmmnModel.getPrimaryCase(); - String startEventType = caseModel.getStartEventType(); - if (startEventType != null) { - restoreEventRegistryStartEvent(previousCaseDefinition, caseModel, startEventType); - } - } + if (latestCaseDefinition == null || !caseDefinition.getId().equals(latestCaseDefinition.getId())) { + return; + } + + // Try to find a previous version (it could be some versions are missing due to deletions) + CaseDefinition previousCaseDefinition = findNewLatestCaseDefinitionAfterRemovalOf(caseDefinition); + if (previousCaseDefinition == null) { + return; } - } - protected void restoreEventRegistryStartEvent(CaseDefinition previousCaseDefinition, Case caseModel, String startEventType) { - engineConfiguration.getEventSubscriptionServiceConfiguration() - .getEventSubscriptionService() - .createEventSubscriptionBuilder() - .eventType(startEventType) - .configuration(CmmnCorrelationUtil.getCorrelationKey(CmmnXmlConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, CommandContextUtil.getCommandContext(), caseModel)) - .scopeDefinitionId(previousCaseDefinition.getId()) - .scopeType(ScopeTypes.CMMN) - .tenantId(previousCaseDefinition.getTenantId()) - .create(); + // The cached parsed model belongs to the deleted case definition; iterate ITS handlers — those + // handlers were also installed on the previous version's parsed model when it was deployed, so + // we can reuse the deleted case's handler list to drive the restore via the previous case's + // own model. + CmmnModel previousCmmnModel = CaseDefinitionUtil.getCmmnModel(previousCaseDefinition.getId()); + Case previousCaseModel = previousCmmnModel.getPrimaryCase(); + + CommandContext commandContext = CommandContextUtil.getCommandContext(); + for (Object handler : previousCaseModel.getStartLifecycleHandlers()) { + if (handler instanceof CaseDefinitionStartLifecycleHandler lifecycleHandler) { + lifecycleHandler.deploy(new CaseDefinitionStartDeployContext( + (CaseDefinitionEntity) previousCaseDefinition, previousCaseModel, + engineConfiguration, commandContext, true)); + } + } } protected CaseDefinitionEntity findLatestCaseDefinition(CaseDefinition caseDefinition) { diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.java new file mode 100644 index 00000000000..c9143c53cbb --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.java @@ -0,0 +1,141 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.test.deployer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.flowable.cmmn.api.repository.CaseDefinition; +import org.flowable.cmmn.api.repository.CmmnDeployment; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionStartDeployContext; +import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionStartLifecycleHandler; +import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionStartUndeployContext; +import org.flowable.cmmn.engine.impl.parser.CmmnParseResult; +import org.flowable.cmmn.engine.impl.parser.CmmnParserImpl; +import org.flowable.cmmn.engine.impl.parser.handler.AbstractCmmnParseHandler; +import org.flowable.cmmn.model.BaseElement; +import org.flowable.cmmn.model.Case; +import org.flowable.cmmn.test.EngineConfigurer; +import org.flowable.cmmn.test.impl.CustomCmmnConfigurationFlowableTestCase; +import org.flowable.common.engine.api.scope.ScopeTypes; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; +import org.junit.jupiter.api.Test; + +/** + * Verifies that a custom {@link CaseDefinitionStartLifecycleHandler} installed via the public + * {@code postCmmnParseHandlers} extension point receives the deploy / undeploy / restore lifecycle + * across deployment, redeployment, and deletion-of-latest. Mirrors the BPMN + * {@code CustomEventDefinitionTest.testCustomEventDefinitionOnProcessStartEvent} scenario. + */ +public class CustomCaseStartLifecycleHandlerTest extends CustomCmmnConfigurationFlowableTestCase { + + private static final String RESOURCE = "org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.cmmn.xml"; + + @EngineConfigurer + protected static void configureConfiguration(CmmnEngineConfiguration cmmnEngineConfiguration) { + cmmnEngineConfiguration.setPostCmmnParseHandlers(Collections.singletonList(new RecordingCaseStartLifecycleParseHandler())); + } + + @Test + public void testDeployRedeployDeleteLifecycle() { + RecordingCaseStartLifecycleHandler.RECORDED.clear(); + + // First deployment. + CmmnDeployment v1 = cmmnRepositoryService.createDeployment().addClasspathResource(RESOURCE).deploy(); + assertThat(RecordingCaseStartLifecycleHandler.RECORDED).containsExactly("deploy@1"); + + // Redeploy — undeploy(v1) before deploy(v2). + CmmnDeployment v2 = cmmnRepositoryService.createDeployment().addClasspathResource(RESOURCE).deploy(); + assertThat(RecordingCaseStartLifecycleHandler.RECORDED).containsExactly("deploy@1", "undeploy@1", "deploy@2"); + + // Delete the latest deployment — restore(v1) via deploy() with the restoringPreviousVersion flag. + cmmnRepositoryService.deleteDeployment(v2.getId(), true); + assertThat(RecordingCaseStartLifecycleHandler.RECORDED).containsExactly("deploy@1", "undeploy@1", "deploy@2", "restore@1"); + + // Cleanup the v1 deployment we created manually. + cmmnRepositoryService.deleteDeployment(v1.getId(), true); + } + + @Test + public void testBulkFlushDeletesObsoleteSubscriptionsOnRedeploy() { + RecordingCaseStartLifecycleHandler.RECORDED.clear(); + + CmmnDeployment v1 = cmmnRepositoryService.createDeployment().addClasspathResource(RESOURCE).deploy(); + String v1CaseDefinitionId = caseDefinitionId(v1.getId()); + assertThat(findSubscriptions(v1CaseDefinitionId)).hasSize(1); + + CmmnDeployment v2 = cmmnRepositoryService.createDeployment().addClasspathResource(RESOURCE).deploy(); + String v2CaseDefinitionId = caseDefinitionId(v2.getId()); + + assertThat(findSubscriptions(v1CaseDefinitionId)).isEmpty(); + assertThat(findSubscriptions(v2CaseDefinitionId)).hasSize(1); + + cmmnRepositoryService.deleteDeployment(v2.getId(), true); + cmmnRepositoryService.deleteDeployment(v1.getId(), true); + } + + private String caseDefinitionId(String deploymentId) { + CaseDefinition caseDefinition = cmmnRepositoryService.createCaseDefinitionQuery() + .deploymentId(deploymentId).singleResult(); + return caseDefinition.getId(); + } + + private List findSubscriptions(String caseDefinitionId) { + return cmmnEngineConfiguration.getCommandExecutor().execute(commandContext -> + cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService() + .findEventSubscriptionsByTypesAndScopeDefinitionId( + Collections.singleton(RecordingCaseStartLifecycleHandler.OBSOLETE_TYPE), + caseDefinitionId, ScopeTypes.CMMN, null)); + } + + public static class RecordingCaseStartLifecycleParseHandler extends AbstractCmmnParseHandler { + @Override + public Collection> getHandledTypes() { + return Collections.singletonList(Case.class); + } + + @Override + protected void executeParse(CmmnParserImpl cmmnParser, CmmnParseResult cmmnParseResult, Case caze) { + caze.addStartLifecycleHandler(new RecordingCaseStartLifecycleHandler()); + } + } + + public static class RecordingCaseStartLifecycleHandler implements CaseDefinitionStartLifecycleHandler { + + public static final String OBSOLETE_TYPE = "myTestCaseStartObsoleteType"; + + public static final List RECORDED = new CopyOnWriteArrayList<>(); + + @Override + public void deploy(CaseDefinitionStartDeployContext context) { + String prefix = context.isRestoringPreviousVersion() ? "restore@" : "deploy@"; + RECORDED.add(prefix + context.getCaseDefinition().getVersion()); + context.getEventSubscriptionService().createEventSubscriptionBuilder() + .eventType(OBSOLETE_TYPE) + .scopeDefinitionId(context.getCaseDefinition().getId()) + .scopeType(ScopeTypes.CMMN) + .create(); + } + + @Override + public void undeploy(CaseDefinitionStartUndeployContext context) { + RECORDED.add("undeploy@" + context.getPreviousCaseDefinition().getVersion()); + context.registerObsoleteEventSubscriptionType(OBSOLETE_TYPE); + } + } +} diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.java new file mode 100644 index 00000000000..3ebbdeb6f81 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.java @@ -0,0 +1,160 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.test.eventlistener; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.flowable.cmmn.api.delegate.DelegatePlanItemInstance; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; +import org.flowable.cmmn.converter.GenericEventListenerXmlConverter; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.behavior.CoreCmmnTriggerableActivityBehavior; +import org.flowable.cmmn.engine.impl.behavior.PlanItemActivityBehavior; +import org.flowable.cmmn.engine.impl.parser.CmmnParseResult; +import org.flowable.cmmn.engine.impl.parser.CmmnParserImpl; +import org.flowable.cmmn.engine.impl.parser.handler.AbstractPlanItemParseHandler; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.cmmn.model.BaseElement; +import org.flowable.cmmn.model.EventListener; +import org.flowable.cmmn.model.PlanItem; +import org.flowable.cmmn.test.EngineConfigurer; +import org.flowable.cmmn.test.impl.CustomCmmnConfigurationFlowableTestCase; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +/** + * Verifies that a custom {@link EventListener} can be wired end-to-end via the public + * {@code customEventListenerTypeFactories} and {@code customCmmnParseHandlers} extension points without + * modifying the engine source. + */ +public class CustomEventListenerTest extends CustomCmmnConfigurationFlowableTestCase { + + @EngineConfigurer + protected static void configureConfiguration(CmmnEngineConfiguration cmmnEngineConfiguration) { + cmmnEngineConfiguration.addCustomEventListenerTypeFactory("myTest", xtr -> { + MyTestEventListener listener = new MyTestEventListener(); + listener.setCustomKey(xtr.getAttributeValue("http://flowable.org/cmmn", "customKey")); + return listener; + }); + cmmnEngineConfiguration.setPostCmmnParseHandlers(Collections.singletonList(new MyTestEventListenerParseHandler())); + } + + @AfterAll + static void cleanupRegistry() { + // addCustomEventListenerTypeFactory wrote into the JVM-static registry on + // GenericEventListenerXmlConverter; closing the engine doesn't undo it. + GenericEventListenerXmlConverter.removeCustomListenerTypeFactory("myTest"); + } + + @Test + public void testCustomEventListener() { + addDeploymentForAutoCleanup(cmmnRepositoryService.createDeployment() + .addClasspathResource("org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.cmmn") + .deploy()); + + // Start the case — the custom listener should land in AVAILABLE state, mirroring how the + // built-in GenericEventListener behaves. + MyTestEventListenerBehavior.OBSERVED_KEYS.clear(); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("customListenerCase").start(); + + PlanItemInstance listenerInstance = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .planItemDefinitionId("customListener") + .singleResult(); + assertThat(listenerInstance).isNotNull(); + assertThat(listenerInstance.getState()).isEqualTo(PlanItemInstanceState.AVAILABLE); + assertThat(MyTestEventListenerBehavior.OBSERVED_KEYS).containsExactly("create-widget-42"); + + // Trigger the listener — our custom behavior records the customKey and routes through OCCUR, + // completing the listener and (since it's the only plan item) the case. + cmmnRuntimeService.triggerPlanItemInstance(listenerInstance.getId()); + + assertThat(MyTestEventListenerBehavior.OBSERVED_KEYS).containsExactly("create-widget-42", "trigger-widget-42", "execute-widget-42", "occur-widget-42"); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isZero(); + } + + // ---- Custom EventListener model ---- + + public static class MyTestEventListener extends EventListener { + + protected String customKey; + + public String getCustomKey() { + return customKey; + } + + public void setCustomKey(String customKey) { + this.customKey = customKey; + } + } + + // ---- Custom parse handler registered via postCmmnParseHandlers ---- + + public static class MyTestEventListenerParseHandler extends AbstractPlanItemParseHandler { + + @Override + public Collection> getHandledTypes() { + return Collections.singletonList(MyTestEventListener.class); + } + + @Override + protected void executePlanItemParse(CmmnParserImpl cmmnParser, CmmnParseResult cmmnParseResult, PlanItem planItem, MyTestEventListener listener) { + // The parse handler knows the typed model, so it constructs the behavior directly — no factory + // dispatch indirection needed. + planItem.setBehavior(new MyTestEventListenerBehavior(listener.getCustomKey())); + } + } + + public static class MyTestEventListenerBehavior extends CoreCmmnTriggerableActivityBehavior implements PlanItemActivityBehavior { + + // Records the keys observed during trigger(). Test-only side channel so we can assert the engine + // actually invoked our behavior end-to-end. + public static final List OBSERVED_KEYS = new CopyOnWriteArrayList<>(); + + protected final String customKey; + + public MyTestEventListenerBehavior(String customKey) { + this.customKey = customKey; + } + + public String getCustomKey() { + return customKey; + } + + @Override + public void onStateTransition(CommandContext commandContext, DelegatePlanItemInstance planItemInstance, String transition) { + OBSERVED_KEYS.add(transition + "-" + customKey); + } + + @Override + public void execute(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + OBSERVED_KEYS.add("execute-" + customKey); + CommandContextUtil.getAgenda(commandContext).planOccurPlanItemInstanceOperation(planItemInstanceEntity); + } + + @Override + public void trigger(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + OBSERVED_KEYS.add("trigger-" + customKey); + execute(commandContext, planItemInstanceEntity); + } + } +} diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.cmmn.xml new file mode 100644 index 00000000000..c648d75684c --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/deployer/CustomCaseStartLifecycleHandlerTest.cmmn.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.cmmn b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.cmmn new file mode 100644 index 00000000000..8d833d5cc54 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/CustomEventListenerTest.cmmn @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/Case.java b/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/Case.java index f79e0d94df6..c6e1ae1b1b9 100644 --- a/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/Case.java +++ b/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/Case.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; + public class Case extends CmmnElement implements HasLifecycleListeners { protected String name; @@ -30,6 +32,14 @@ public class Case extends CmmnElement implements HasLifecycleListeners { protected List lifecycleListeners = new ArrayList<>(); /** Having a reactivation case listener marks the case eligible for reactivation once completed. */ protected ReactivateEventListener reactivateEventListener; + /** + * Engine-side handlers that own the case's deploy / undeploy lifecycle for whatever start trigger + * the case has (event-registry subscription by default; custom integrations can install additional + * handlers via a custom CMMN parse handler). Typed as {@code Object} so this model module stays + * engine-dep-free; the engine casts on iteration. Transient — not part of the parsed XML. + */ + @JsonIgnore + protected List startLifecycleHandlers = new ArrayList<>(); public String getName() { return name; @@ -116,4 +126,16 @@ public List getLifecycleListeners() { public void setLifecycleListeners(List lifecycleListeners) { this.lifecycleListeners = lifecycleListeners; } + + public List getStartLifecycleHandlers() { + return startLifecycleHandlers; + } + + public void setStartLifecycleHandlers(List startLifecycleHandlers) { + this.startLifecycleHandlers = startLifecycleHandlers; + } + + public void addStartLifecycleHandler(Object handler) { + this.startLifecycleHandlers.add(handler); + } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventRegistryStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventRegistryStartEventActivityBehavior.java new file mode 100644 index 00000000000..1295ffaced6 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventRegistryStartEventActivityBehavior.java @@ -0,0 +1,87 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.common.engine.api.scope.ScopeTypes; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.CorrelationUtil; +import org.flowable.engine.impl.util.CountingEntityUtil; +import org.flowable.eventsubscription.api.EventSubscription; +import org.flowable.eventsubscription.api.EventSubscriptionBuilder; +import org.flowable.eventsubscription.service.EventSubscriptionService; + +/** + * Process-level event-registry start event behavior. Owns the deploy-time event-registry subscription + * registration for this start event, including the dynamic-correlation re-point logic when the + * process definition is superseded. + */ +public class EventRegistryStartEventActivityBehavior extends FlowNodeActivityBehavior implements ProcessLevelStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + protected String eventDefinitionKey; + protected boolean manualCorrelation; + + public EventRegistryStartEventActivityBehavior(String eventDefinitionKey, boolean manualCorrelation) { + this.eventDefinitionKey = eventDefinitionKey; + this.manualCorrelation = manualCorrelation; + } + + @Override + public void deploy(ProcessLevelStartEventDeployContext context) { + // dynamic, manual subscription mode: deploy does not create a subscription — those are added + // explicitly by the application at runtime. + if (manualCorrelation) { + return; + } + + ProcessDefinitionEntity processDefinition = context.getProcessDefinition(); + EventSubscriptionService eventSubscriptionService = context.getEventSubscriptionService(); + EventSubscriptionBuilder eventSubscriptionBuilder = eventSubscriptionService.createEventSubscriptionBuilder() + .eventType(eventDefinitionKey) + .activityId(context.getStartEvent().getId()) + .processDefinitionId(processDefinition.getId()) + .scopeType(ScopeTypes.BPMN) + .configuration(CorrelationUtil.getCorrelationKey(BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, context.getCommandContext(), context.getStartEvent(), null)); + + if (processDefinition.getTenantId() != null) { + eventSubscriptionBuilder.tenantId(processDefinition.getTenantId()); + } + + EventSubscription eventSubscription = eventSubscriptionBuilder.create(); + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); + } + + @Override + public void undeploy(ProcessLevelStartEventUndeployContext context) { + if (manualCorrelation) { + // dynamic mode: keep existing subscriptions but re-point them to the new process definition instead of deleting. + ProcessDefinitionEntity previousProcessDefinition = context.getPreviousProcessDefinition(); + ProcessDefinitionEntity newProcessDefinition = context.getNewProcessDefinition(); + context.getProcessEngineConfiguration().getEventSubscriptionServiceConfiguration().getEventSubscriptionService().updateEventSubscriptionProcessDefinitionId( + previousProcessDefinition.getId(), newProcessDefinition.getId(), + eventDefinitionKey, context.getStartEvent().getId(), newProcessDefinition.getKey(), null); + } else { + context.registerObsoleteEventSubscriptionType(eventDefinitionKey); + } + } + + public String getEventDefinitionKey() { + return eventDefinitionKey; + } + + public boolean isManualCorrelation() { + return manualCorrelation; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessEventRegistryStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessEventRegistryStartEventActivityBehavior.java index 327e7d25eb4..5010d5ae787 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessEventRegistryStartEventActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessEventRegistryStartEventActivityBehavior.java @@ -20,10 +20,13 @@ import java.util.Map; import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.SubProcess; import org.flowable.bpmn.model.ValuedDataObject; +import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.context.Context; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.delegate.DelegateExecution; @@ -34,6 +37,7 @@ import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager; import org.flowable.engine.impl.util.CommandContextUtil; +import org.flowable.engine.impl.util.CorrelationUtil; import org.flowable.engine.impl.util.CountingEntityUtil; import org.flowable.eventregistry.api.runtime.EventInstance; import org.flowable.eventregistry.impl.constant.EventConstants; @@ -45,7 +49,7 @@ * * @author Tijs Rademakers */ -public class EventSubProcessEventRegistryStartEventActivityBehavior extends AbstractBpmnActivityBehavior { +public class EventSubProcessEventRegistryStartEventActivityBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { private static final long serialVersionUID = 1L; @@ -55,6 +59,23 @@ public EventSubProcessEventRegistryStartEventActivityBehavior(String eventDefini this.eventDefinitionKey = eventDefinitionKey; } + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + if (StringUtils.isEmpty(eventDefinitionKey)) { + return; + } + + ExecutionEntity eventRegistryExecution = context.createEventScopeChildExecution(); + + EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) context.createEventSubscriptionBuilder(eventRegistryExecution) + .eventType(eventDefinitionKey) + .scopeType(ScopeTypes.BPMN) + .configuration(CorrelationUtil.getCorrelationKey(BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, context.getCommandContext(), eventRegistryExecution)) + .create(); + + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); + } + @Override public void execute(DelegateExecution execution) { StartEvent startEvent = (StartEvent) execution.getCurrentFlowElement(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessMessageStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessMessageStartEventActivityBehavior.java index e11479e9087..4428c6369b6 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessMessageStartEventActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessMessageStartEventActivityBehavior.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.MessageEventDefinition; import org.flowable.bpmn.model.StartEvent; @@ -35,6 +36,7 @@ import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.impl.util.CountingEntityUtil; +import org.flowable.engine.impl.util.ProcessDefinitionUtil; import org.flowable.eventsubscription.service.EventSubscriptionService; import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; @@ -44,7 +46,7 @@ * * @author Tijs Rademakers */ -public class EventSubProcessMessageStartEventActivityBehavior extends AbstractBpmnActivityBehavior { +public class EventSubProcessMessageStartEventActivityBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { private static final long serialVersionUID = 1L; @@ -54,6 +56,27 @@ public EventSubProcessMessageStartEventActivityBehavior(MessageEventDefinition m this.messageEventDefinition = messageEventDefinition; } + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + ExecutionEntity parentExecution = context.getParentExecution(); + BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(parentExecution.getProcessDefinitionId()); + if (bpmnModel.containsMessageId(messageEventDefinition.getMessageRef())) { + messageEventDefinition.setMessageRef(bpmnModel.getMessage(messageEventDefinition.getMessageRef()).getName()); + } + + ExecutionEntity messageExecution = context.createEventScopeChildExecution(); + + String messageName = EventDefinitionExpressionUtil.determineMessageName(context.getCommandContext(), messageEventDefinition, parentExecution); + EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) context.createEventSubscriptionBuilder(messageExecution) + .eventType(MessageEventSubscriptionEntity.EVENT_TYPE) + .eventName(messageName) + .create(); + + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); + context.recordWaitingMessageSubscription(eventSubscription); + messageExecution.getEventSubscriptions().add(eventSubscription); + } + @Override public void execute(DelegateExecution execution) { StartEvent startEvent = (StartEvent) execution.getCurrentFlowElement(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessSignalStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessSignalStartEventActivityBehavior.java index ca12bcfdc45..63f2a0bed47 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessSignalStartEventActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessSignalStartEventActivityBehavior.java @@ -19,13 +19,16 @@ import java.util.List; import java.util.Map; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.Signal; import org.flowable.bpmn.model.SignalEventDefinition; import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.SubProcess; import org.flowable.bpmn.model.ValuedDataObject; +import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.context.Context; +import org.flowable.common.engine.impl.el.DefinitionVariableContainer; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.ExecutionListener; @@ -46,7 +49,7 @@ * * @author Tijs Rademakers */ -public class EventSubProcessSignalStartEventActivityBehavior extends AbstractBpmnActivityBehavior { +public class EventSubProcessSignalStartEventActivityBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { private static final long serialVersionUID = 1L; @@ -58,6 +61,31 @@ public EventSubProcessSignalStartEventActivityBehavior(SignalEventDefinition sig this.signal = signal; } + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + ExecutionEntity parentExecution = context.getParentExecution(); + if (signal != null) { + signalEventDefinition.setSignalRef(signal.getName()); + } + + ExecutionEntity signalExecution = context.createEventScopeChildExecution(); + + BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(parentExecution.getProcessDefinitionId()); + DefinitionVariableContainer definitionVariableContainer = new DefinitionVariableContainer(parentExecution.getProcessDefinitionId(), + parentExecution.getProcessDefinitionKey(), parentExecution.getDeploymentId(), ScopeTypes.BPMN, parentExecution.getTenantId()); + String eventName = EventDefinitionExpressionUtil.determineSignalName(context.getCommandContext(), signalEventDefinition, bpmnModel, definitionVariableContainer); + + EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) context.createEventSubscriptionBuilder(signalExecution) + .eventType(SignalEventSubscriptionEntity.EVENT_TYPE) + .eventName(eventName) + .signal(signal) + .create(); + + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); + context.recordWaitingSignalSubscription(eventSubscription); + signalExecution.getEventSubscriptions().add(eventSubscription); + } + @Override public void execute(DelegateExecution execution) { StartEvent startEvent = (StartEvent) execution.getCurrentFlowElement(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventActivityBehavior.java new file mode 100644 index 00000000000..1e996bdf8db --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventActivityBehavior.java @@ -0,0 +1,29 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +/** + * Implemented by an {@code ActivityBehavior} attached to an event-sub-process {@code StartEvent} that + * registers a subscription, timer or waiting execution when the parent process or sub-process becomes + * active. The {@code execute()} / {@code trigger()} half of the lifecycle continues to live on the + * existing {@code ActivityBehavior} interface; this interface adds the "register at parent start" + * half so each behavior owns its full lifecycle. + *

+ * Custom integrations that supply their own {@code EventDefinition} + parse handler + behavior opt + * into event-sub-process subscription registration by implementing this interface — no change in + * {@code ProcessInstanceHelper} is required. + */ +public interface EventSubProcessStartEventActivityBehavior { + + void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context); +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventInitializerContext.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventInitializerContext.java new file mode 100644 index 00000000000..aee13692dc8 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessStartEventInitializerContext.java @@ -0,0 +1,115 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import java.util.List; + +import org.flowable.bpmn.model.StartEvent; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.persistence.entity.ExecutionEntity; +import org.flowable.eventsubscription.api.EventSubscriptionBuilder; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; + +/** + * Context passed to {@link EventSubProcessStartEventActivityBehavior#initializeEventSubProcessStart} + * when a parent process or sub-process becomes active and its event-sub-process start events need + * to register their subscriptions / timers / waiting executions. + */ +public class EventSubProcessStartEventInitializerContext { + + protected final ExecutionEntity parentExecution; + protected final StartEvent startEvent; + protected final ProcessEngineConfigurationImpl processEngineConfiguration; + protected final CommandContext commandContext; + protected final List messageEventSubscriptions; + protected final List signalEventSubscriptions; + + public EventSubProcessStartEventInitializerContext(ExecutionEntity parentExecution, StartEvent startEvent, + ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext, + List messageEventSubscriptions, List signalEventSubscriptions) { + this.parentExecution = parentExecution; + this.startEvent = startEvent; + this.processEngineConfiguration = processEngineConfiguration; + this.commandContext = commandContext; + this.messageEventSubscriptions = messageEventSubscriptions; + this.signalEventSubscriptions = signalEventSubscriptions; + } + + public ExecutionEntity getParentExecution() { + return parentExecution; + } + + public StartEvent getStartEvent() { + return startEvent; + } + + public ProcessEngineConfigurationImpl getProcessEngineConfiguration() { + return processEngineConfiguration; + } + + public CommandContext getCommandContext() { + return commandContext; + } + + /** + * Returns an {@link EventSubscriptionBuilder} pre-filled with the execution / process-instance / + * activity / process-definition / tenant ids of the supplied event-scope execution. Subscription- + * registering initializers chain on it with the type-specific fields (eventType, eventName, etc.) + * before calling {@code create()}. + */ + public EventSubscriptionBuilder createEventSubscriptionBuilder(ExecutionEntity eventScopeExecution) { + return processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService() + .createEventSubscriptionBuilder() + .executionId(eventScopeExecution.getId()) + .processInstanceId(eventScopeExecution.getProcessInstanceId()) + .activityId(eventScopeExecution.getCurrentActivityId()) + .processDefinitionId(eventScopeExecution.getProcessDefinitionId()) + .tenantId(eventScopeExecution.getTenantId()); + } + + /** + * Creates a child execution of the parent that represents the waiting state for this event-sub-process + * start event: pointed at the start event, marked as event scope, and inactive. This is the four-line + * pattern repeated by every built-in event-sub-process start initializer. + */ + public ExecutionEntity createEventScopeChildExecution() { + ExecutionEntity execution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); + execution.setCurrentFlowElement(startEvent); + execution.setEventScope(true); + execution.setActive(false); + return execution; + } + + /** + * Records a message event subscription so that {@code processEventSubProcess} can dispatch + * {@code ACTIVITY_MESSAGE_WAITING} for it after every start event in the same sub-process has + * registered. No-op if the caller didn't supply a collection (e.g. dynamic state migration paths). + */ + public void recordWaitingMessageSubscription(EventSubscriptionEntity subscription) { + if (messageEventSubscriptions != null) { + messageEventSubscriptions.add(subscription); + } + } + + /** + * Records a signal event subscription so that {@code processEventSubProcess} can dispatch + * {@code ACTIVITY_SIGNAL_WAITING} for it after every start event in the same sub-process has + * registered. No-op if the caller didn't supply a collection (e.g. dynamic state migration paths). + */ + public void recordWaitingSignalSubscription(EventSubscriptionEntity subscription) { + if (signalEventSubscriptions != null) { + signalEventSubscriptions.add(subscription); + } + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessTimerStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessTimerStartEventActivityBehavior.java index ddd47b21c46..8719f896e3f 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessTimerStartEventActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessTimerStartEventActivityBehavior.java @@ -29,16 +29,20 @@ import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.history.DeleteReason; +import org.flowable.engine.impl.jobexecutor.TimerEventHandler; +import org.flowable.engine.impl.jobexecutor.TriggerTimerEventJobHandler; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager; import org.flowable.engine.impl.util.CommandContextUtil; +import org.flowable.engine.impl.util.TimerUtil; +import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; /** * Implementation of the BPMN 2.0 event subprocess timer start event. * * @author Tijs Rademakers */ -public class EventSubProcessTimerStartEventActivityBehavior extends AbstractBpmnActivityBehavior { +public class EventSubProcessTimerStartEventActivityBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { private static final long serialVersionUID = 1L; @@ -48,6 +52,18 @@ public EventSubProcessTimerStartEventActivityBehavior(TimerEventDefinition timer this.timerEventDefinition = timerEventDefinition; } + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + StartEvent startEvent = context.getStartEvent(); + ExecutionEntity timerExecution = context.createEventScopeChildExecution(); + + TimerJobEntity timerJob = TimerUtil.createTimerEntityForTimerEventDefinition(timerEventDefinition, startEvent, + false, timerExecution, TriggerTimerEventJobHandler.TYPE, TimerEventHandler.createConfiguration(startEvent.getId(), + timerEventDefinition.getEndDate(), timerEventDefinition.getCalendarName())); + + context.getProcessEngineConfiguration().getJobServiceConfiguration().getTimerJobService().scheduleTimerJob(timerJob); + } + @Override public void execute(DelegateExecution execution) { StartEvent startEvent = (StartEvent) execution.getCurrentFlowElement(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessVariableListenerlStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessVariableListenerlStartEventActivityBehavior.java index e19d5e4d658..3f10d8c1907 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessVariableListenerlStartEventActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/EventSubProcessVariableListenerlStartEventActivityBehavior.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.SubProcess; @@ -38,12 +39,14 @@ import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; import org.flowable.eventsubscription.service.impl.persistence.entity.GenericEventSubscriptionEntity; +import tools.jackson.databind.node.ObjectNode; + /** * Implementation of the event subprocess variable listener start event. * * @author Tijs Rademakers */ -public class EventSubProcessVariableListenerlStartEventActivityBehavior extends AbstractBpmnActivityBehavior { +public class EventSubProcessVariableListenerlStartEventActivityBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { private static final long serialVersionUID = 1L; @@ -53,6 +56,27 @@ public EventSubProcessVariableListenerlStartEventActivityBehavior(VariableListen this.variableListenerEventDefinition = variableListenerEventDefinition; } + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + ExecutionEntity variableListenerExecution = context.createEventScopeChildExecution(); + + String configuration = null; + if (StringUtils.isNotEmpty(variableListenerEventDefinition.getVariableChangeType())) { + ObjectNode configurationNode = context.getProcessEngineConfiguration().getObjectMapper().createObjectNode(); + configurationNode.put(VariableListenerEventDefinition.CHANGE_TYPE_PROPERTY, variableListenerEventDefinition.getVariableChangeType()); + configuration = configurationNode.toString(); + } + + EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) context.createEventSubscriptionBuilder(variableListenerExecution) + .eventType("variable") + .eventName(variableListenerEventDefinition.getVariableName()) + .configuration(configuration) + .create(); + + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); + variableListenerExecution.getEventSubscriptions().add(eventSubscription); + } + @Override public void execute(DelegateExecution execution) { StartEvent startEvent = (StartEvent) execution.getCurrentFlowElement(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/MessageStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/MessageStartEventActivityBehavior.java new file mode 100644 index 00000000000..5114f518296 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/MessageStartEventActivityBehavior.java @@ -0,0 +1,90 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import java.util.List; + +import org.flowable.bpmn.model.MessageEventDefinition; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; +import org.flowable.engine.impl.event.MessageEventHandler; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.CountingEntityUtil; +import org.flowable.eventsubscription.service.EventSubscriptionService; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; +import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; + +/** + * Process-level message start event behavior. Owns the deploy-time message subscription registration + * for this start event. + */ +public class MessageStartEventActivityBehavior extends FlowNodeActivityBehavior implements ProcessLevelStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + protected MessageEventDefinition messageEventDefinition; + + public MessageStartEventActivityBehavior(MessageEventDefinition messageEventDefinition) { + this.messageEventDefinition = messageEventDefinition; + } + + @Override + public void deploy(ProcessLevelStartEventDeployContext context) { + CommandContext commandContext = context.getCommandContext(); + EventSubscriptionService eventSubscriptionService = context.getEventSubscriptionService(); + ProcessDefinitionEntity processDefinition = context.getProcessDefinition(); + + String messageName = EventDefinitionExpressionUtil.determineMessageName(commandContext, messageEventDefinition, processDefinition); + + // Skip the duplicate check when restoring a previous version's start events after the latest + // deployment was deleted: the just-deleted process definition's subscription is still in the in-session entity + // cache (the bulk delete in deleteEventSubscriptionsForProcessDefinition is queued, not flushed) + // and would trip a false-positive conflict. + if (!context.isRestoringPreviousVersion()) { + List subscriptionsForSameMessageName = eventSubscriptionService + .findEventSubscriptionsByName(MessageEventHandler.EVENT_HANDLER_TYPE, messageName, processDefinition.getTenantId()); + + for (EventSubscriptionEntity eventSubscriptionEntity : subscriptionsForSameMessageName) { + // throw exception only if there's already a subscription as start event + if (eventSubscriptionEntity.getProcessInstanceId() == null || eventSubscriptionEntity.getProcessInstanceId().isEmpty()) { + // the event subscription has no instance-id, so it's a message start event + throw new FlowableException("Cannot deploy process definition '" + processDefinition.getResourceName() + + "': there already is a message event subscription for the message with name '" + messageName + "'. For " + eventSubscriptionEntity); + } + } + } + + MessageEventSubscriptionEntity newSubscription = eventSubscriptionService.createMessageEventSubscription(); + newSubscription.setEventName(messageName); + newSubscription.setActivityId(context.getStartEvent().getId()); + newSubscription.setConfiguration(processDefinition.getId()); + newSubscription.setProcessDefinitionId(processDefinition.getId()); + + if (processDefinition.getTenantId() != null) { + newSubscription.setTenantId(processDefinition.getTenantId()); + } + + eventSubscriptionService.insertEventSubscription(newSubscription); + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(newSubscription); + } + + @Override + public void undeploy(ProcessLevelStartEventUndeployContext context) { + context.registerObsoleteEventSubscriptionType(MessageEventHandler.EVENT_HANDLER_TYPE); + } + + public MessageEventDefinition getMessageEventDefinition() { + return messageEventDefinition; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventActivityBehavior.java new file mode 100644 index 00000000000..0d051a58f86 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventActivityBehavior.java @@ -0,0 +1,43 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +/** + * Implemented by an {@code ActivityBehavior} attached to a process-level (i.e. NOT inside an + * {@code EventSubProcess}) {@code StartEvent} that registers a deploy-time artifact — an event + * subscription, a start timer job, etc. — and removes or updates that artifact when the process + * definition is superseded by a new version. + *

+ * Both {@link #deploy} and {@link #undeploy} must be implemented; a behavior that opts into the + * deploy-time lifecycle owns both halves of it. Built-in start event types that don't register + * anything (e.g. none / error) simply don't implement this interface. + *

+ * Custom integrations supplying their own {@code EventDefinition} + parse handler + behavior pick + * up deploy-time registration by implementing this interface — no change in + * {@code BpmnDeploymentHelper} is required. + */ +public interface ProcessLevelStartEventActivityBehavior { + + /** + * Register the deploy-time artifact (event subscription, timer job, etc.) for this start event + * when its process definition is freshly deployed. + */ + void deploy(ProcessLevelStartEventDeployContext context); + + /** + * Remove or update the deploy-time artifact for this start event when its process definition is + * superseded by a new version. Called on the previous (now-superseded) process definition's + * start events. + */ + void undeploy(ProcessLevelStartEventUndeployContext context); +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventDeployContext.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventDeployContext.java new file mode 100644 index 00000000000..942e7b36bad --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventDeployContext.java @@ -0,0 +1,94 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.eventsubscription.service.EventSubscriptionService; + +/** + * Context passed to {@link ProcessLevelStartEventActivityBehavior#deploy} carrying the freshly-deployed + * process definition together with its parsed model and the start event in scope. + */ +public class ProcessLevelStartEventDeployContext { + + protected final ProcessDefinitionEntity processDefinition; + protected final Process process; + protected final BpmnModel bpmnModel; + protected final StartEvent startEvent; + protected final ProcessEngineConfigurationImpl processEngineConfiguration; + protected final CommandContext commandContext; + protected final boolean restoringPreviousVersion; + + public ProcessLevelStartEventDeployContext(ProcessDefinitionEntity processDefinition, + Process process, BpmnModel bpmnModel, StartEvent startEvent, + ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext) { + this(processDefinition, process, bpmnModel, startEvent, processEngineConfiguration, commandContext, false); + } + + public ProcessLevelStartEventDeployContext(ProcessDefinitionEntity processDefinition, + Process process, BpmnModel bpmnModel, StartEvent startEvent, + ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext, + boolean restoringPreviousVersion) { + this.processDefinition = processDefinition; + this.process = process; + this.bpmnModel = bpmnModel; + this.startEvent = startEvent; + this.processEngineConfiguration = processEngineConfiguration; + this.commandContext = commandContext; + this.restoringPreviousVersion = restoringPreviousVersion; + } + + public ProcessDefinitionEntity getProcessDefinition() { + return processDefinition; + } + + public Process getProcess() { + return process; + } + + public BpmnModel getBpmnModel() { + return bpmnModel; + } + + public StartEvent getStartEvent() { + return startEvent; + } + + public ProcessEngineConfigurationImpl getProcessEngineConfiguration() { + return processEngineConfiguration; + } + + public CommandContext getCommandContext() { + return commandContext; + } + + public EventSubscriptionService getEventSubscriptionService() { + return processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); + } + + /** + * {@code true} when this deploy is restoring a previous (earlier-version) process definition's start + * events because the latest version's deployment was just deleted. Behaviors should skip duplicate- + * subscription validation in this mode — the just-deleted process definition's subscriptions are still in the in- + * session entity cache (the bulk delete hasn't been flushed yet) so a re-insert would otherwise + * trip a false-positive conflict. The fresh-deployment path leaves this {@code false}. + */ + public boolean isRestoringPreviousVersion() { + return restoringPreviousVersion; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventUndeployContext.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventUndeployContext.java new file mode 100644 index 00000000000..18a5f9ca2b7 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/ProcessLevelStartEventUndeployContext.java @@ -0,0 +1,95 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import java.util.Set; + +import org.flowable.bpmn.model.StartEvent; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; + +/** + * Context passed to {@link ProcessLevelStartEventActivityBehavior#undeploy} when a process definition + * is being superseded by a new version. Carries both the previous (now-superseded) process definition + * and the new one — most behaviors only need the previous process definition, but the EventRegistry "manual" re-point + * branch updates subscriptions to point at {@link #getNewProcessDefinition()}. + *

+ * Behaviors register their obsolete event subscription / timer job handler types via + * {@link #registerObsoleteEventSubscriptionType(String)} and + * {@link #registerObsoleteTimerJobHandlerType(String)}. The deployer issues one mass-delete per + * unique registered type after the undeploy iteration — fewer DB round-trips than per-start-event + * deletes, and tighter than the historical fixed Message+Signal+EventRegistry sweep that always + * ran regardless of which types the previous process definition actually used. + */ +public class ProcessLevelStartEventUndeployContext { + + protected final ProcessDefinitionEntity previousProcessDefinition; + protected final ProcessDefinitionEntity newProcessDefinition; + protected final StartEvent startEvent; + protected final ProcessEngineConfigurationImpl processEngineConfiguration; + protected final CommandContext commandContext; + protected final Set obsoleteEventSubscriptionTypes; + protected final Set obsoleteTimerJobHandlerTypes; + + public ProcessLevelStartEventUndeployContext(ProcessDefinitionEntity previousProcessDefinition, ProcessDefinitionEntity newProcessDefinition, + StartEvent startEvent, + ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext, + Set obsoleteEventSubscriptionTypes, Set obsoleteTimerJobHandlerTypes) { + this.previousProcessDefinition = previousProcessDefinition; + this.newProcessDefinition = newProcessDefinition; + this.startEvent = startEvent; + this.processEngineConfiguration = processEngineConfiguration; + this.commandContext = commandContext; + this.obsoleteEventSubscriptionTypes = obsoleteEventSubscriptionTypes; + this.obsoleteTimerJobHandlerTypes = obsoleteTimerJobHandlerTypes; + } + + public ProcessDefinitionEntity getPreviousProcessDefinition() { + return previousProcessDefinition; + } + + public ProcessDefinitionEntity getNewProcessDefinition() { + return newProcessDefinition; + } + + public StartEvent getStartEvent() { + return startEvent; + } + + public ProcessEngineConfigurationImpl getProcessEngineConfiguration() { + return processEngineConfiguration; + } + + public CommandContext getCommandContext() { + return commandContext; + } + + /** + * Registers an event subscription handler type that the deployer should mass-delete for the + * previous process definition after the undeploy pass. Multiple behaviors registering the same + * type result in a single DB sweep. + */ + public void registerObsoleteEventSubscriptionType(String eventHandlerType) { + obsoleteEventSubscriptionTypes.add(eventHandlerType); + } + + /** + * Registers a timer job handler type that the deployer should mass-cancel for the previous + * process definition after the undeploy pass. Multiple behaviors registering the same type + * result in a single DB sweep. + */ + public void registerObsoleteTimerJobHandlerType(String timerJobHandlerType) { + obsoleteTimerJobHandlerTypes.add(timerJobHandlerType); + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/SignalStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/SignalStartEventActivityBehavior.java new file mode 100644 index 00000000000..94810a31e1e --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/SignalStartEventActivityBehavior.java @@ -0,0 +1,73 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import org.flowable.bpmn.model.Signal; +import org.flowable.bpmn.model.SignalEventDefinition; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; +import org.flowable.engine.impl.event.SignalEventHandler; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.CountingEntityUtil; +import org.flowable.eventsubscription.service.EventSubscriptionService; +import org.flowable.eventsubscription.service.impl.persistence.entity.SignalEventSubscriptionEntity; + +/** + * Process-level signal start event behavior. Owns the deploy-time signal subscription registration + * for this start event. + */ +public class SignalStartEventActivityBehavior extends FlowNodeActivityBehavior implements ProcessLevelStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + protected SignalEventDefinition signalEventDefinition; + protected Signal signal; + + public SignalStartEventActivityBehavior(SignalEventDefinition signalEventDefinition, Signal signal) { + this.signalEventDefinition = signalEventDefinition; + this.signal = signal; + } + + @Override + public void deploy(ProcessLevelStartEventDeployContext context) { + CommandContext commandContext = context.getCommandContext(); + EventSubscriptionService eventSubscriptionService = context.getEventSubscriptionService(); + ProcessDefinitionEntity processDefinition = context.getProcessDefinition(); + SignalEventSubscriptionEntity subscriptionEntity = eventSubscriptionService.createSignalEventSubscription(); + + String signalName = EventDefinitionExpressionUtil.determineSignalName(commandContext, signalEventDefinition, context.getBpmnModel(), processDefinition); + subscriptionEntity.setEventName(signalName); + + subscriptionEntity.setActivityId(context.getStartEvent().getId()); + subscriptionEntity.setProcessDefinitionId(processDefinition.getId()); + if (processDefinition.getTenantId() != null) { + subscriptionEntity.setTenantId(processDefinition.getTenantId()); + } + + eventSubscriptionService.insertEventSubscription(subscriptionEntity); + CountingEntityUtil.handleInsertEventSubscriptionEntityCount(subscriptionEntity); + } + + @Override + public void undeploy(ProcessLevelStartEventUndeployContext context) { + context.registerObsoleteEventSubscriptionType(SignalEventHandler.EVENT_HANDLER_TYPE); + } + + public SignalEventDefinition getSignalEventDefinition() { + return signalEventDefinition; + } + + public Signal getSignal() { + return signal; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/TimerStartEventActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/TimerStartEventActivityBehavior.java new file mode 100644 index 00000000000..e32d902ab98 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/TimerStartEventActivityBehavior.java @@ -0,0 +1,55 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.behavior; + +import org.flowable.bpmn.model.TimerEventDefinition; +import org.flowable.engine.impl.jobexecutor.TimerEventHandler; +import org.flowable.engine.impl.jobexecutor.TimerStartEventJobHandler; +import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.flowable.engine.impl.util.TimerUtil; +import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; + +/** + * Process-level timer start event behavior. Owns the deploy-time timer job scheduling for this + * start event. + */ +public class TimerStartEventActivityBehavior extends FlowNodeActivityBehavior implements ProcessLevelStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + protected TimerEventDefinition timerEventDefinition; + + public TimerStartEventActivityBehavior(TimerEventDefinition timerEventDefinition) { + this.timerEventDefinition = timerEventDefinition; + } + + @Override + public void deploy(ProcessLevelStartEventDeployContext context) { + ProcessDefinitionEntity processDefinition = context.getProcessDefinition(); + TimerJobEntity timerJob = TimerUtil.createTimerEntityForTimerEventDefinition(timerEventDefinition, context.getStartEvent(), + false, processDefinition, TimerStartEventJobHandler.TYPE, + TimerEventHandler.createConfiguration(context.getStartEvent().getId(), + timerEventDefinition.getEndDate(), timerEventDefinition.getCalendarName())); + + context.getProcessEngineConfiguration().getJobServiceConfiguration().getTimerJobService().scheduleTimerJob(timerJob); + } + + @Override + public void undeploy(ProcessLevelStartEventUndeployContext context) { + context.registerObsoleteTimerJobHandlerType(TimerStartEventJobHandler.TYPE); + } + + public TimerEventDefinition getTimerEventDefinition() { + return timerEventDefinition; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeploymentHelper.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeploymentHelper.java index 78e873520df..473ba3bc0b9 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeploymentHelper.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeploymentHelper.java @@ -20,7 +20,9 @@ import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.StartEvent; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.impl.assignment.CandidateUtil; @@ -28,14 +30,24 @@ import org.flowable.common.engine.impl.el.ExpressionManager; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.ProcessEngineConfiguration; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventDeployContext; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventUndeployContext; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.persistence.entity.DeploymentEntity; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityManager; import org.flowable.engine.impl.util.CommandContextUtil; +import org.flowable.engine.impl.util.CountingEntityUtil; +import org.flowable.engine.impl.util.ProcessDefinitionUtil; +import org.flowable.eventsubscription.service.EventSubscriptionService; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; import org.flowable.identitylink.api.IdentityLinkType; import org.flowable.identitylink.service.IdentityLinkService; import org.flowable.identitylink.service.impl.persistence.entity.IdentityLinkEntity; +import org.flowable.job.service.TimerJobService; +import org.flowable.job.service.impl.cmd.CancelJobsCmd; +import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; import org.flowable.variable.service.impl.el.NoExecutionVariableScope; /** @@ -44,9 +56,6 @@ */ public class BpmnDeploymentHelper { - protected TimerManager timerManager; - protected EventSubscriptionManager eventSubscriptionManager; - /** * Verifies that no two process definitions share the same key, to prevent database unique index violation. * @@ -161,21 +170,95 @@ public ProcessDefinitionEntity getPersistedInstanceOfProcessDefinition(ProcessDe } /** - * Updates all timers and events for the process definition. This removes obsolete message and signal subscriptions and timers, and adds new ones. + * Updates all timers and events for the process definition. The undeploy half iterates the previous + * process definition's top-level start events; each behavior either does its own per-start-event work + * (e.g. the EventRegistry "manual" re-point) or registers an obsolete event subscription / timer job + * handler type with the context. After the undeploy iteration the deployer issues one mass-delete per + * unique registered type — fewer DB round-trips than per-start-event deletes, and tighter than the + * historical fixed Message+Signal+EventRegistry sweep that always ran regardless of which types the + * previous process definition used. The deploy half then iterates the new process definition's top-level start events to register the + * new artifacts. */ public void updateTimersAndEvents(ProcessDefinitionEntity processDefinition, ProcessDefinitionEntity previousProcessDefinition, ParsedDeployment parsedDeployment) { + CommandContext commandContext = Context.getCommandContext(); + ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); + Process process = parsedDeployment.getProcessModelForProcessDefinition(processDefinition); BpmnModel bpmnModel = parsedDeployment.getBpmnModelForProcessDefinition(processDefinition); - eventSubscriptionManager.removeObsoleteMessageEventSubscriptions(previousProcessDefinition); - eventSubscriptionManager.removeObsoleteSignalEventSubscription(previousProcessDefinition); - eventSubscriptionManager.removeOrUpdateObsoleteEventRegistryEventSubscription(previousProcessDefinition, processDefinition); - eventSubscriptionManager.addEventSubscriptions(processDefinition, process, bpmnModel); + if (previousProcessDefinition != null) { + Process previousProcess = ProcessDefinitionUtil.getProcess(previousProcessDefinition.getId()); + Set obsoleteEventSubscriptionTypes = new LinkedHashSet<>(); + Set obsoleteTimerJobHandlerTypes = new LinkedHashSet<>(); + + forEachTopLevelStartEventBehavior(previousProcess, (startEvent, behavior) -> behavior.undeploy( + new ProcessLevelStartEventUndeployContext(previousProcessDefinition, processDefinition, + startEvent, processEngineConfiguration, commandContext, + obsoleteEventSubscriptionTypes, obsoleteTimerJobHandlerTypes))); + + if (!obsoleteEventSubscriptionTypes.isEmpty()) { + deleteObsoleteEventSubscriptions(previousProcessDefinition, obsoleteEventSubscriptionTypes, processEngineConfiguration); + } + for (String timerJobHandlerType : obsoleteTimerJobHandlerTypes) { + cancelObsoleteTimerJobs(previousProcessDefinition, timerJobHandlerType, processEngineConfiguration, commandContext); + } + } + + forEachTopLevelStartEventBehavior(process, (startEvent, behavior) -> behavior.deploy( + new ProcessLevelStartEventDeployContext(processDefinition, + process, bpmnModel, startEvent, processEngineConfiguration, commandContext))); + } + + protected void deleteObsoleteEventSubscriptions(ProcessDefinitionEntity processDefinition, Collection eventHandlerTypes, + ProcessEngineConfigurationImpl processEngineConfiguration) { + + EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); + List subscriptionsToDelete = eventSubscriptionService + .findEventSubscriptionsByTypesAndProcessDefinitionId(eventHandlerTypes, processDefinition.getId(), processDefinition.getTenantId()); - timerManager.removeObsoleteTimers(processDefinition); - timerManager.scheduleTimers(processDefinition, process); + for (EventSubscriptionEntity eventSubscription : subscriptionsToDelete) { + eventSubscriptionService.deleteEventSubscription(eventSubscription); + CountingEntityUtil.handleDeleteEventSubscriptionEntityCount(eventSubscription); + } + } + + protected void cancelObsoleteTimerJobs(ProcessDefinitionEntity processDefinition, String timerJobHandlerType, + ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext) { + + TimerJobService timerJobService = processEngineConfiguration.getJobServiceConfiguration().getTimerJobService(); + List jobsToDelete; + if (processDefinition.getTenantId() != null && !ProcessEngineConfiguration.NO_TENANT_ID.equals(processDefinition.getTenantId())) { + jobsToDelete = timerJobService.findJobsByTypeAndProcessDefinitionKeyAndTenantId( + timerJobHandlerType, processDefinition.getKey(), processDefinition.getTenantId()); + } else { + jobsToDelete = timerJobService.findJobsByTypeAndProcessDefinitionKeyNoTenantId( + timerJobHandlerType, processDefinition.getKey()); + } + + if (jobsToDelete != null) { + for (TimerJobEntity job : jobsToDelete) { + new CancelJobsCmd(job.getId(), processEngineConfiguration.getJobServiceConfiguration()).execute(commandContext); + } + } + } + + protected void forEachTopLevelStartEventBehavior(Process process, StartEventBehaviorVisitor visitor) { + if (process == null || process.getFlowElements() == null) { + return; + } + for (FlowElement flowElement : process.getFlowElements()) { + if (flowElement instanceof StartEvent startEvent + && startEvent.getBehavior() instanceof ProcessLevelStartEventActivityBehavior behavior) { + visitor.visit(startEvent, behavior); + } + } + } + + @FunctionalInterface + protected interface StartEventBehaviorVisitor { + void visit(StartEvent startEvent, ProcessLevelStartEventActivityBehavior behavior); } enum ExpressionType { @@ -227,19 +310,4 @@ protected void addAuthorizationsFromIterator(CommandContext commandContext, List } - public TimerManager getTimerManager() { - return timerManager; - } - - public void setTimerManager(TimerManager timerManager) { - this.timerManager = timerManager; - } - - public EventSubscriptionManager getEventSubscriptionManager() { - return eventSubscriptionManager; - } - - public void setEventSubscriptionManager(EventSubscriptionManager eventSubscriptionManager) { - this.eventSubscriptionManager = eventSubscriptionManager; - } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/EventSubscriptionManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/EventSubscriptionManager.java deleted file mode 100644 index 06c447af540..00000000000 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/EventSubscriptionManager.java +++ /dev/null @@ -1,250 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.flowable.engine.impl.bpmn.deployer; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.ExtensionElement; -import org.flowable.bpmn.model.FlowElement; -import org.flowable.bpmn.model.MessageEventDefinition; -import org.flowable.bpmn.model.Process; -import org.flowable.bpmn.model.SignalEventDefinition; -import org.flowable.bpmn.model.StartEvent; -import org.flowable.common.engine.api.FlowableException; -import org.flowable.common.engine.api.scope.ScopeTypes; -import org.flowable.common.engine.impl.context.Context; -import org.flowable.common.engine.impl.interceptor.CommandContext; -import org.flowable.common.engine.impl.util.CollectionUtil; -import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; -import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; -import org.flowable.engine.impl.event.MessageEventHandler; -import org.flowable.engine.impl.event.SignalEventHandler; -import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; -import org.flowable.engine.impl.util.CommandContextUtil; -import org.flowable.engine.impl.util.CorrelationUtil; -import org.flowable.engine.impl.util.CountingEntityUtil; -import org.flowable.engine.impl.util.ProcessDefinitionUtil; -import org.flowable.eventsubscription.api.EventSubscription; -import org.flowable.eventsubscription.api.EventSubscriptionBuilder; -import org.flowable.eventsubscription.service.EventSubscriptionService; -import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; -import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; -import org.flowable.eventsubscription.service.impl.persistence.entity.SignalEventSubscriptionEntity; - -/** - * Manages event subscriptions for newly-deployed process definitions and their previous versions. - */ -public class EventSubscriptionManager { - - protected void removeObsoleteMessageEventSubscriptions(ProcessDefinitionEntity previousProcessDefinition) { - // remove all subscriptions for the previous version - if (previousProcessDefinition != null) { - removeObsoleteEventSubscriptions(previousProcessDefinition, MessageEventHandler.EVENT_HANDLER_TYPE); - } - } - - protected void removeObsoleteSignalEventSubscription(ProcessDefinitionEntity previousProcessDefinition) { - // remove all subscriptions for the previous version - if (previousProcessDefinition != null) { - removeObsoleteEventSubscriptions(previousProcessDefinition, SignalEventHandler.EVENT_HANDLER_TYPE); - } - } - - protected void removeOrUpdateObsoleteEventRegistryEventSubscription(ProcessDefinitionEntity previousProcessDefinition, ProcessDefinitionEntity processDefinition) { - // remove all subscriptions for the previous version or update them for a dynamic, manual subscription behavior - if (previousProcessDefinition != null) { - List eventRegistryStartEvents = getEventRegistryStartEventEventTypes(previousProcessDefinition); - if (eventRegistryStartEvents != null) { - for (StartEventInfo eventRegistryStartEvent : eventRegistryStartEvents) { - if (eventRegistryStartEvent.dynamic()) { - // for a dynamic, manual subscription behavior, we must not remove the old subscriptions, but rather update them - // to the newest process definition id, as they have been manually added before - updateOldEventSubscriptions(previousProcessDefinition, processDefinition, eventRegistryStartEvent.eventType(), eventRegistryStartEvent.activityId()); - } else { - // for a static starting behavior, we always remove the old subscription and recreate it with the new definition - removeObsoleteEventSubscriptions(previousProcessDefinition, eventRegistryStartEvent.eventType()); - } - } - } - } - } - - protected List getEventRegistryStartEventEventTypes(ProcessDefinitionEntity previousProcessDefinition) { - List result = null; - Process process = ProcessDefinitionUtil.getProcess(previousProcessDefinition.getId()); - List startEvents = process.findFlowElementsOfType(StartEvent.class, true); - if (!startEvents.isEmpty()) { - for (StartEvent startEvent : startEvents) { - if (CollectionUtil.isEmpty(startEvent.getEventDefinitions())) { - List eventTypeElements = startEvent.getExtensionElements().get("eventType"); - if (eventTypeElements != null && !eventTypeElements.isEmpty()) { - String eventType = eventTypeElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventType)) { - if (result == null) { - result = new ArrayList<>(); - } - - // check the starting behavior of the event-registry start event, if it is dynamic with manual subscriptions, add it to the - // result with a true boolean, otherwise with false - List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); - if (correlationConfiguration != null && correlationConfiguration.size() > 0 && BpmnXMLConstants.START_EVENT_CORRELATION_MANUAL.equals(correlationConfiguration.get(0).getElementText())) { - result.add(new StartEventInfo(eventType, startEvent.getId(), true)); - } else { - result.add(new StartEventInfo(eventType, startEvent.getId(), false)); - } - } - } - } - } - } - return result; - } - - protected record StartEventInfo(String eventType, String activityId, boolean dynamic) { - } - - protected void removeObsoleteEventSubscriptions(ProcessDefinitionEntity processDefinition, String eventHandlerType) { - // remove all subscriptions for the previous version - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); - EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - List subscriptionsToDelete = eventSubscriptionService - .findEventSubscriptionsByTypeAndProcessDefinitionId(eventHandlerType, processDefinition.getId(), processDefinition.getTenantId()); - - for (EventSubscriptionEntity eventSubscriptionEntity : subscriptionsToDelete) { - eventSubscriptionService.deleteEventSubscription(eventSubscriptionEntity); - CountingEntityUtil.handleDeleteEventSubscriptionEntityCount(eventSubscriptionEntity); - } - } - - protected void updateOldEventSubscriptions(ProcessDefinitionEntity previousProcessDefinition, ProcessDefinitionEntity processDefinition, - String eventType, String activityId) { - CommandContext commandContext = Context.getCommandContext(); - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); - - processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService().updateEventSubscriptionProcessDefinitionId( - previousProcessDefinition.getId(), processDefinition.getId(), eventType, activityId, processDefinition.getKey(), null); - } - - protected void addEventSubscriptions(ProcessDefinitionEntity processDefinition, org.flowable.bpmn.model.Process process, BpmnModel bpmnModel) { - if (CollectionUtil.isNotEmpty(process.getFlowElements())) { - for (FlowElement element : process.getFlowElements()) { - if (element instanceof StartEvent startEvent) { - if (CollectionUtil.isNotEmpty(startEvent.getEventDefinitions())) { - EventDefinition eventDefinition = startEvent.getEventDefinitions().get(0); - if (eventDefinition instanceof SignalEventDefinition signalEventDefinition) { - insertSignalEvent(signalEventDefinition, startEvent, processDefinition, bpmnModel); - - } else if (eventDefinition instanceof MessageEventDefinition messageEventDefinition) { - insertMessageEvent(messageEventDefinition, startEvent, processDefinition, bpmnModel); - } - - } else { - if (startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE) != null) { - List eventTypeElements = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (!eventTypeElements.isEmpty()) { - String eventDefinitionKey = eventTypeElements.get(0).getElementText(); - insertEventRegistryEvent(eventDefinitionKey, startEvent, processDefinition, bpmnModel); - } - } - } - } - } - } - } - - protected void insertSignalEvent(SignalEventDefinition signalEventDefinition, StartEvent startEvent, ProcessDefinitionEntity processDefinition, BpmnModel bpmnModel) { - CommandContext commandContext = Context.getCommandContext(); - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); - EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - SignalEventSubscriptionEntity subscriptionEntity = eventSubscriptionService.createSignalEventSubscription(); - - String signalName = EventDefinitionExpressionUtil.determineSignalName(commandContext, signalEventDefinition, bpmnModel, processDefinition); - subscriptionEntity.setEventName(signalName); - - subscriptionEntity.setActivityId(startEvent.getId()); - subscriptionEntity.setProcessDefinitionId(processDefinition.getId()); - if (processDefinition.getTenantId() != null) { - subscriptionEntity.setTenantId(processDefinition.getTenantId()); - } - - eventSubscriptionService.insertEventSubscription(subscriptionEntity); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(subscriptionEntity); - } - - protected void insertMessageEvent(MessageEventDefinition messageEventDefinition, StartEvent startEvent, ProcessDefinitionEntity processDefinition, BpmnModel bpmnModel) { - CommandContext commandContext = Context.getCommandContext(); - - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); - EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - - // look for subscriptions for the same name in db: - String messageName = EventDefinitionExpressionUtil.determineMessageName(commandContext, messageEventDefinition, processDefinition); - List subscriptionsForSameMessageName = eventSubscriptionService - .findEventSubscriptionsByName(MessageEventHandler.EVENT_HANDLER_TYPE, messageName, processDefinition.getTenantId()); - - for (EventSubscriptionEntity eventSubscriptionEntity : subscriptionsForSameMessageName) { - // throw exception only if there's already a subscription as start event - if (eventSubscriptionEntity.getProcessInstanceId() == null || eventSubscriptionEntity.getProcessInstanceId().isEmpty()) { // processInstanceId != null or not empty -> it's a message related to an execution - // the event subscription has no instance-id, so it's a message start event - throw new FlowableException("Cannot deploy process definition '" + processDefinition.getResourceName() - + "': there already is a message event subscription for the message with name '" + messageName + "'. For " + eventSubscriptionEntity); - } - } - - MessageEventSubscriptionEntity newSubscription = eventSubscriptionService.createMessageEventSubscription(); - newSubscription.setEventName(messageName); - newSubscription.setActivityId(startEvent.getId()); - newSubscription.setConfiguration(processDefinition.getId()); - newSubscription.setProcessDefinitionId(processDefinition.getId()); - - if (processDefinition.getTenantId() != null) { - newSubscription.setTenantId(processDefinition.getTenantId()); - } - - eventSubscriptionService.insertEventSubscription(newSubscription); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(newSubscription); - } - - protected void insertEventRegistryEvent(String eventDefinitionKey, StartEvent startEvent, ProcessDefinitionEntity processDefinition, BpmnModel bpmnModel) { - // check, if we have a dynamic event-based start for that process definition - List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); - if (correlationConfiguration != null && correlationConfiguration.size() > 0) { - if (BpmnXMLConstants.START_EVENT_CORRELATION_MANUAL.equals(correlationConfiguration.get(0).getElementText())) { - return; - } - } - - CommandContext commandContext = Context.getCommandContext(); - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); - EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - EventSubscriptionBuilder eventSubscriptionBuilder = eventSubscriptionService.createEventSubscriptionBuilder() - .eventType(eventDefinitionKey) - .activityId(startEvent.getId()) - .processDefinitionId(processDefinition.getId()) - .scopeType(ScopeTypes.BPMN) - .configuration(CorrelationUtil.getCorrelationKey(BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, commandContext, startEvent, null)); - - if (processDefinition.getTenantId() != null) { - eventSubscriptionBuilder.tenantId(processDefinition.getTenantId()); - } - - EventSubscription eventSubscription = eventSubscriptionBuilder.create(); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); - } - -} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/TimerManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/TimerManager.java deleted file mode 100644 index 5db18d6fde3..00000000000 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/TimerManager.java +++ /dev/null @@ -1,91 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.flowable.engine.impl.bpmn.deployer; - -import java.util.ArrayList; -import java.util.List; - -import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.FlowElement; -import org.flowable.bpmn.model.Process; -import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.TimerEventDefinition; -import org.flowable.common.engine.impl.context.Context; -import org.flowable.common.engine.impl.util.CollectionUtil; -import org.flowable.engine.ProcessEngineConfiguration; -import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; -import org.flowable.engine.impl.jobexecutor.TimerEventHandler; -import org.flowable.engine.impl.jobexecutor.TimerStartEventJobHandler; -import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; -import org.flowable.engine.impl.util.CommandContextUtil; -import org.flowable.engine.impl.util.TimerUtil; -import org.flowable.job.service.TimerJobService; -import org.flowable.job.service.impl.cmd.CancelJobsCmd; -import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; - -/** - * Manages timers for newly-deployed process definitions and their previous versions. - */ -public class TimerManager { - - protected void removeObsoleteTimers(ProcessDefinitionEntity processDefinition) { - List jobsToDelete = null; - - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); - if (processDefinition.getTenantId() != null && !ProcessEngineConfiguration.NO_TENANT_ID.equals(processDefinition.getTenantId())) { - jobsToDelete = processEngineConfiguration.getJobServiceConfiguration().getTimerJobService().findJobsByTypeAndProcessDefinitionKeyAndTenantId( - TimerStartEventJobHandler.TYPE, processDefinition.getKey(), processDefinition.getTenantId()); - } else { - jobsToDelete = processEngineConfiguration.getJobServiceConfiguration().getTimerJobService() - .findJobsByTypeAndProcessDefinitionKeyNoTenantId(TimerStartEventJobHandler.TYPE, processDefinition.getKey()); - } - - if (jobsToDelete != null) { - for (TimerJobEntity job : jobsToDelete) { - new CancelJobsCmd(job.getId(), processEngineConfiguration.getJobServiceConfiguration()).execute(Context.getCommandContext()); - } - } - } - - protected void scheduleTimers(ProcessDefinitionEntity processDefinition, Process process) { - TimerJobService timerJobService = CommandContextUtil.getTimerJobService(); - List timers = getTimerDeclarations(processDefinition, process); - for (TimerJobEntity timer : timers) { - timerJobService.scheduleTimerJob(timer); - } - } - - protected List getTimerDeclarations(ProcessDefinitionEntity processDefinition, Process process) { - List timers = new ArrayList<>(); - if (CollectionUtil.isNotEmpty(process.getFlowElements())) { - for (FlowElement element : process.getFlowElements()) { - if (element instanceof StartEvent startEvent) { - if (CollectionUtil.isNotEmpty(startEvent.getEventDefinitions())) { - EventDefinition eventDefinition = startEvent.getEventDefinitions().get(0); - if (eventDefinition instanceof TimerEventDefinition timerEventDefinition) { - - TimerJobEntity timerJob = TimerUtil.createTimerEntityForTimerEventDefinition(timerEventDefinition, startEvent, - false, processDefinition, TimerStartEventJobHandler.TYPE, TimerEventHandler.createConfiguration(startEvent.getId(), - timerEventDefinition.getEndDate(), timerEventDefinition.getCalendarName())); - - timers.add(timerJob); - - } - } - } - } - } - - return timers; - } -} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/ActivityBehaviorFactory.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/ActivityBehaviorFactory.java index 075e666d0b8..2fddfdd7749 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/ActivityBehaviorFactory.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/ActivityBehaviorFactory.java @@ -92,7 +92,11 @@ import org.flowable.engine.impl.bpmn.behavior.IntermediateThrowSignalEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ManualTaskActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneEndEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventRegistryStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.MessageStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.SignalStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.TimerStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelGatewayActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.ReceiveEventTaskActivityBehavior; @@ -136,6 +140,14 @@ public interface ActivityBehaviorFactory { NoneStartEventActivityBehavior createNoneStartEventActivityBehavior(StartEvent startEvent); + MessageStartEventActivityBehavior createMessageStartEventActivityBehavior(StartEvent startEvent, MessageEventDefinition messageEventDefinition); + + SignalStartEventActivityBehavior createSignalStartEventActivityBehavior(StartEvent startEvent, SignalEventDefinition signalEventDefinition, Signal signal); + + TimerStartEventActivityBehavior createTimerStartEventActivityBehavior(StartEvent startEvent, TimerEventDefinition timerEventDefinition); + + EventRegistryStartEventActivityBehavior createEventRegistryStartEventActivityBehavior(StartEvent startEvent, String eventDefinitionKey, boolean manualCorrelation); + TaskActivityBehavior createTaskActivityBehavior(Task task); ManualTaskActivityBehavior createManualTaskActivityBehavior(ManualTask manualTask); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/DefaultActivityBehaviorFactory.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/DefaultActivityBehaviorFactory.java index 74d68097195..8da92f5b110 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/DefaultActivityBehaviorFactory.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/factory/DefaultActivityBehaviorFactory.java @@ -106,7 +106,11 @@ import org.flowable.engine.impl.bpmn.behavior.IntermediateThrowSignalEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ManualTaskActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneEndEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventRegistryStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.MessageStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.SignalStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.TimerStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelGatewayActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.ReceiveEventTaskActivityBehavior; @@ -156,6 +160,26 @@ public NoneStartEventActivityBehavior createNoneStartEventActivityBehavior(Start return new NoneStartEventActivityBehavior(); } + @Override + public MessageStartEventActivityBehavior createMessageStartEventActivityBehavior(StartEvent startEvent, MessageEventDefinition messageEventDefinition) { + return new MessageStartEventActivityBehavior(messageEventDefinition); + } + + @Override + public SignalStartEventActivityBehavior createSignalStartEventActivityBehavior(StartEvent startEvent, SignalEventDefinition signalEventDefinition, Signal signal) { + return new SignalStartEventActivityBehavior(signalEventDefinition, signal); + } + + @Override + public TimerStartEventActivityBehavior createTimerStartEventActivityBehavior(StartEvent startEvent, TimerEventDefinition timerEventDefinition) { + return new TimerStartEventActivityBehavior(timerEventDefinition); + } + + @Override + public EventRegistryStartEventActivityBehavior createEventRegistryStartEventActivityBehavior(StartEvent startEvent, String eventDefinitionKey, boolean manualCorrelation) { + return new EventRegistryStartEventActivityBehavior(eventDefinitionKey, manualCorrelation); + } + // Task @Override diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/BoundaryEventParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/BoundaryEventParseHandler.java index ea6626b478b..7e72439f253 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/BoundaryEventParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/BoundaryEventParseHandler.java @@ -12,23 +12,10 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; -import org.flowable.bpmn.model.CancelEventDefinition; -import org.flowable.bpmn.model.CompensateEventDefinition; -import org.flowable.bpmn.model.ConditionalEventDefinition; -import org.flowable.bpmn.model.ErrorEventDefinition; -import org.flowable.bpmn.model.EscalationEventDefinition; import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.ExtensionElement; -import org.flowable.bpmn.model.MessageEventDefinition; -import org.flowable.bpmn.model.SignalEventDefinition; -import org.flowable.bpmn.model.TimerEventDefinition; -import org.flowable.bpmn.model.VariableListenerEventDefinition; +import org.flowable.bpmn.model.EventDefinitionLocation; import org.flowable.engine.impl.bpmn.parser.BpmnParse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,39 +41,20 @@ protected void executeParse(BpmnParse bpmnParse, BoundaryEvent boundaryEvent) { return; } - EventDefinition eventDefinition = null; - if (boundaryEvent.getEventDefinitions().size() > 0) { - eventDefinition = boundaryEvent.getEventDefinitions().get(0); + if (boundaryEvent.getEventDefinitions().isEmpty()) { + // Should already be picked up by process validator on deploy, so this is just to be sure + LOGGER.warn("Unsupported boundary event type for boundary event {}", boundaryEvent.getId()); + return; } - if (eventDefinition instanceof TimerEventDefinition || - eventDefinition instanceof ErrorEventDefinition || - eventDefinition instanceof SignalEventDefinition || - eventDefinition instanceof CancelEventDefinition || - eventDefinition instanceof ConditionalEventDefinition || - eventDefinition instanceof MessageEventDefinition || - eventDefinition instanceof EscalationEventDefinition || - eventDefinition instanceof CompensateEventDefinition || - eventDefinition instanceof VariableListenerEventDefinition) { - - bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); + EventDefinition eventDefinition = boundaryEvent.getEventDefinitions().get(0); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.BOUNDARY_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on boundary event {}", + eventDefinition.getClass().getSimpleName(), boundaryEvent.getId()); return; - - } else if (!boundaryEvent.getExtensionElements().isEmpty()) { - List eventTypeExtensionElements = boundaryEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeExtensionElements != null && !eventTypeExtensionElements.isEmpty()) { - String eventTypeValue = eventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventTypeValue)) { - boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryEventRegistryEventActivityBehavior( - boundaryEvent, eventTypeValue, boundaryEvent.isCancelActivity())); - return; - } - } + } - } - - // Should already be picked up by process validator on deploy, so this is just to be sure - LOGGER.warn("Unsupported boundary event type for boundary event {}", boundaryEvent.getId()); + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/CancelEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/CancelEventDefinitionParseHandler.java index d129188b6fb..6bd27ead4ea 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/CancelEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/CancelEventDefinitionParseHandler.java @@ -15,6 +15,7 @@ import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; import org.flowable.bpmn.model.CancelEventDefinition; +import org.flowable.bpmn.model.EndEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; /** @@ -32,7 +33,9 @@ public Class getHandledType() { protected void executeParse(BpmnParse bpmnParse, CancelEventDefinition cancelEventDefinition) { if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryCancelEventActivityBehavior(cancelEventDefinition)); - } + } else if (bpmnParse.getCurrentFlowElement() instanceof EndEvent endEvent) { + endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createCancelEndEventActivityBehavior(endEvent)); + } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EndEventParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EndEventParseHandler.java index f4295bba2cb..331f49143bb 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EndEventParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EndEventParseHandler.java @@ -1,9 +1,9 @@ /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -12,15 +12,10 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; -import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BaseElement; -import org.flowable.bpmn.model.CancelEventDefinition; import org.flowable.bpmn.model.EndEvent; -import org.flowable.bpmn.model.ErrorEventDefinition; -import org.flowable.bpmn.model.Escalation; -import org.flowable.bpmn.model.EscalationEventDefinition; import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.TerminateEventDefinition; +import org.flowable.bpmn.model.EventDefinitionLocation; import org.flowable.engine.impl.bpmn.parser.BpmnParse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,42 +35,19 @@ public Class getHandledType() { @Override protected void executeParse(BpmnParse bpmnParse, EndEvent endEvent) { + if (endEvent.getEventDefinitions().isEmpty()) { + endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createNoneEndEventActivityBehavior(endEvent)); + return; + } - EventDefinition eventDefinition = null; - if (endEvent.getEventDefinitions().size() > 0) { - eventDefinition = endEvent.getEventDefinitions().get(0); - - if (eventDefinition instanceof ErrorEventDefinition errorDefinition) { - if (bpmnParse.getBpmnModel().containsErrorRef(errorDefinition.getErrorCode())) { - String errorCode = bpmnParse.getBpmnModel().getErrors().get(errorDefinition.getErrorCode()); - if (StringUtils.isEmpty(errorCode)) { - LOGGER.warn("errorCode is required for an error event {}", endEvent.getId()); - } - } - endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createErrorEndEventActivityBehavior(endEvent, errorDefinition)); - - } else if (eventDefinition instanceof EscalationEventDefinition escalationDefinition) { - Escalation escalation = null; - if (bpmnParse.getBpmnModel().containsEscalationRef(escalationDefinition.getEscalationCode())) { - escalation = bpmnParse.getBpmnModel().getEscalation(escalationDefinition.getEscalationCode()); - String escalationCode = escalation.getEscalationCode(); - if (StringUtils.isEmpty(escalationCode)) { - LOGGER.warn("escalationCode is required for an escalation event {}", endEvent.getId()); - } - } - endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createEscalationEndEventActivityBehavior(endEvent, escalationDefinition, escalation)); - - } else if (eventDefinition instanceof TerminateEventDefinition) { - endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createTerminateEndEventActivityBehavior(endEvent)); - } else if (eventDefinition instanceof CancelEventDefinition) { - endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createCancelEndEventActivityBehavior(endEvent)); - } else { - endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createNoneEndEventActivityBehavior(endEvent)); - } - - } else { + EventDefinition eventDefinition = endEvent.getEventDefinitions().get(0); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.END_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on end event {}; falling back to none-end behavior", + eventDefinition.getClass().getSimpleName(), endEvent.getId()); endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createNoneEndEventActivityBehavior(endEvent)); + return; } - } + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); + } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/ErrorEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/ErrorEventDefinitionParseHandler.java index 7800ea28cb8..6b94d93474b 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/ErrorEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/ErrorEventDefinitionParseHandler.java @@ -12,10 +12,16 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EndEvent; import org.flowable.bpmn.model.ErrorEventDefinition; +import org.flowable.bpmn.model.EventSubProcess; +import org.flowable.bpmn.model.StartEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Joram Barrez @@ -23,6 +29,8 @@ */ public class ErrorEventDefinitionParseHandler extends AbstractBpmnParseHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(ErrorEventDefinitionParseHandler.class); + @Override public Class getHandledType() { return ErrorEventDefinition.class; @@ -32,6 +40,20 @@ public Class getHandledType() { protected void executeParse(BpmnParse bpmnParse, ErrorEventDefinition eventDefinition) { if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryEventActivityBehavior(boundaryEvent, true)); + + } else if (bpmnParse.getCurrentFlowElement() instanceof EndEvent endEvent) { + if (bpmnParse.getBpmnModel().containsErrorRef(eventDefinition.getErrorCode())) { + String errorCode = bpmnParse.getBpmnModel().getErrors().get(eventDefinition.getErrorCode()); + if (StringUtils.isEmpty(errorCode)) { + LOGGER.warn("errorCode is required for an error event {}", endEvent.getId()); + } + } + endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createErrorEndEventActivityBehavior(endEvent, eventDefinition)); + + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent + && startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessErrorStartEventActivityBehavior(startEvent)); } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EscalationEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EscalationEventDefinitionParseHandler.java index cbbf7861b58..e3808e0249c 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EscalationEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EscalationEventDefinitionParseHandler.java @@ -12,17 +12,26 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EndEvent; import org.flowable.bpmn.model.Escalation; import org.flowable.bpmn.model.EscalationEventDefinition; +import org.flowable.bpmn.model.EventSubProcess; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.bpmn.model.ThrowEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Tijs Rademakers */ public class EscalationEventDefinitionParseHandler extends AbstractBpmnParseHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(EscalationEventDefinitionParseHandler.class); + @Override public Class getHandledType() { return EscalationEventDefinition.class; @@ -39,6 +48,27 @@ protected void executeParse(BpmnParse bpmnParse, EscalationEventDefinition event boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryEscalationEventActivityBehavior(boundaryEvent, eventDefinition, escalation, boundaryEvent.isCancelActivity())); + + } else if (bpmnParse.getCurrentFlowElement() instanceof ThrowEvent throwEvent) { + Escalation escalation = bpmnParse.getBpmnModel().getEscalation(eventDefinition.getEscalationCode()); + throwEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createIntermediateThrowEscalationEventActivityBehavior(throwEvent, eventDefinition, escalation)); + + } else if (bpmnParse.getCurrentFlowElement() instanceof EndEvent endEvent) { + Escalation escalation = null; + if (bpmnParse.getBpmnModel().containsEscalationRef(eventDefinition.getEscalationCode())) { + escalation = bpmnParse.getBpmnModel().getEscalation(eventDefinition.getEscalationCode()); + if (StringUtils.isEmpty(escalation.getEscalationCode())) { + LOGGER.warn("escalationCode is required for an escalation event {}", endEvent.getId()); + } + } + endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEscalationEndEventActivityBehavior(endEvent, eventDefinition, escalation)); + + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent + && startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessEscalationStartEventActivityBehavior(startEvent)); } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EventRegistryEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EventRegistryEventDefinitionParseHandler.java new file mode 100644 index 00000000000..3b52b3513d4 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/EventRegistryEventDefinitionParseHandler.java @@ -0,0 +1,76 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.parser.handler; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.BaseElement; +import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EventRegistryEventDefinition; +import org.flowable.bpmn.model.EventSubProcess; +import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.common.engine.api.FlowableIllegalArgumentException; +import org.flowable.engine.impl.bpmn.parser.BpmnParse; + +public class EventRegistryEventDefinitionParseHandler extends AbstractBpmnParseHandler { + + @Override + public Class getHandledType() { + return EventRegistryEventDefinition.class; + } + + @Override + protected void executeParse(BpmnParse bpmnParse, EventRegistryEventDefinition eventDefinition) { + FlowElement currentFlowElement = bpmnParse.getCurrentFlowElement(); + if (currentFlowElement instanceof IntermediateCatchEvent intermediateCatchEvent) { + String key = requireEventDefinitionKey(eventDefinition, intermediateCatchEvent.getId()); + intermediateCatchEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createIntermediateCatchEventRegistryEventActivityBehavior(intermediateCatchEvent, key)); + + } else if (currentFlowElement instanceof BoundaryEvent boundaryEvent) { + String key = requireEventDefinitionKey(eventDefinition, boundaryEvent.getId()); + boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createBoundaryEventRegistryEventActivityBehavior(boundaryEvent, key, boundaryEvent.isCancelActivity())); + + } else if (currentFlowElement instanceof StartEvent startEvent) { + String key = requireEventDefinitionKey(eventDefinition, startEvent.getId()); + if (startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessEventRegistryStartEventActivityBehavior(startEvent, key)); + } else { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventRegistryStartEventActivityBehavior(startEvent, key, isManualCorrelation(startEvent))); + } + } + } + + protected static boolean isManualCorrelation(StartEvent startEvent) { + List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); + return correlationConfiguration != null && !correlationConfiguration.isEmpty() + && BpmnXMLConstants.START_EVENT_CORRELATION_MANUAL.equals(correlationConfiguration.get(0).getElementText()); + } + + private static String requireEventDefinitionKey(EventRegistryEventDefinition eventRegistry, String elementId) { + String key = eventRegistry.getEventDefinitionKey(); + if (StringUtils.isEmpty(key)) { + throw new FlowableIllegalArgumentException("EventRegistryEventDefinition on '" + elementId + + "' has an empty eventDefinitionKey; the engine cannot register an event-registry subscription without a key."); + } + return key; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateCatchEventParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateCatchEventParseHandler.java index 938e197687a..4f7f7a1b1b4 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateCatchEventParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateCatchEventParseHandler.java @@ -12,20 +12,10 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.BaseElement; -import org.flowable.bpmn.model.ConditionalEventDefinition; import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.EventDefinitionLocation; import org.flowable.bpmn.model.IntermediateCatchEvent; -import org.flowable.bpmn.model.MessageEventDefinition; -import org.flowable.bpmn.model.SignalEventDefinition; -import org.flowable.bpmn.model.TimerEventDefinition; -import org.flowable.bpmn.model.VariableListenerEventDefinition; import org.flowable.engine.impl.bpmn.parser.BpmnParse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,40 +35,19 @@ public Class getHandledType() { @Override protected void executeParse(BpmnParse bpmnParse, IntermediateCatchEvent intermediateCatchEvent) { - EventDefinition eventDefinition = null; - if (!intermediateCatchEvent.getEventDefinitions().isEmpty()) { - eventDefinition = intermediateCatchEvent.getEventDefinitions().get(0); - } - - if (eventDefinition == null) { - - Map> extensionElements = intermediateCatchEvent.getExtensionElements(); - if (!extensionElements.isEmpty()) { - List eventTypeExtensionElements = intermediateCatchEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeExtensionElements != null && !eventTypeExtensionElements.isEmpty()) { - String eventTypeValue = eventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventTypeValue)) { - intermediateCatchEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateCatchEventRegistryEventActivityBehavior(intermediateCatchEvent, eventTypeValue)); - return; - } - } - } - + if (intermediateCatchEvent.getEventDefinitions().isEmpty()) { intermediateCatchEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateCatchEventActivityBehavior(intermediateCatchEvent)); + return; + } - } else { - if (eventDefinition instanceof TimerEventDefinition || - eventDefinition instanceof SignalEventDefinition || - eventDefinition instanceof MessageEventDefinition || - eventDefinition instanceof ConditionalEventDefinition || - eventDefinition instanceof VariableListenerEventDefinition) { - - bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); - - } else { - LOGGER.warn("Unsupported intermediate catch event type for event {}", intermediateCatchEvent.getId()); - } + EventDefinition eventDefinition = intermediateCatchEvent.getEventDefinitions().get(0); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on intermediate catch event {}", + eventDefinition.getClass().getSimpleName(), intermediateCatchEvent.getId()); + return; } + + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateThrowEventParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateThrowEventParseHandler.java index cf66fb990b5..7c1f1a6ef2a 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateThrowEventParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/IntermediateThrowEventParseHandler.java @@ -1,9 +1,9 @@ /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,10 +13,8 @@ package org.flowable.engine.impl.bpmn.parser.handler; import org.flowable.bpmn.model.BaseElement; -import org.flowable.bpmn.model.CompensateEventDefinition; -import org.flowable.bpmn.model.EscalationEventDefinition; import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.SignalEventDefinition; +import org.flowable.bpmn.model.EventDefinitionLocation; import org.flowable.bpmn.model.ThrowEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; import org.slf4j.Logger; @@ -36,27 +34,18 @@ public Class getHandledType() { @Override protected void executeParse(BpmnParse bpmnParse, ThrowEvent intermediateEvent) { - - EventDefinition eventDefinition = null; - if (!intermediateEvent.getEventDefinitions().isEmpty()) { - eventDefinition = intermediateEvent.getEventDefinitions().get(0); + if (intermediateEvent.getEventDefinitions().isEmpty()) { + intermediateEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateThrowNoneEventActivityBehavior(intermediateEvent)); + return; } - if (eventDefinition instanceof SignalEventDefinition signalEventDefinition) { - intermediateEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateThrowSignalEventActivityBehavior(intermediateEvent, signalEventDefinition, - bpmnParse.getBpmnModel().getSignal(signalEventDefinition.getSignalRef()))); - - } else if (eventDefinition instanceof EscalationEventDefinition escalationEventDefinition) { - intermediateEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateThrowEscalationEventActivityBehavior(intermediateEvent, escalationEventDefinition, - bpmnParse.getBpmnModel().getEscalation(escalationEventDefinition.getEscalationCode()))); - - } else if (eventDefinition instanceof CompensateEventDefinition compensateEventDefinition) { - intermediateEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateThrowCompensationEventActivityBehavior(intermediateEvent, compensateEventDefinition)); - - } else if (eventDefinition == null) { - intermediateEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createIntermediateThrowNoneEventActivityBehavior(intermediateEvent)); - } else { - LOGGER.warn("Unsupported intermediate throw event type for throw event {}", intermediateEvent.getId()); + EventDefinition eventDefinition = intermediateEvent.getEventDefinitions().get(0); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.INTERMEDIATE_THROW_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on intermediate throw event {}", + eventDefinition.getClass().getSimpleName(), intermediateEvent.getId()); + return; } + + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/MessageEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/MessageEventDefinitionParseHandler.java index 4840a1d58ce..a13fa75fe85 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/MessageEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/MessageEventDefinitionParseHandler.java @@ -17,10 +17,12 @@ import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.IntermediateCatchEvent; import org.flowable.bpmn.model.Message; import org.flowable.bpmn.model.MessageEventDefinition; +import org.flowable.bpmn.model.StartEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; /** @@ -54,12 +56,16 @@ protected void executeParse(BpmnParse bpmnParse, MessageEventDefinition messageD } else if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryMessageEventActivityBehavior(boundaryEvent, messageDefinition, boundaryEvent.isCancelActivity())); - } - else { - // What to do here? + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent) { + if (startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessMessageStartEventActivityBehavior(startEvent, messageDefinition)); + } else { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createMessageStartEventActivityBehavior(startEvent, messageDefinition)); + } } - } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/SignalEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/SignalEventDefinitionParseHandler.java index dd5b2277dc7..55717667c2b 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/SignalEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/SignalEventDefinitionParseHandler.java @@ -14,9 +14,12 @@ import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.IntermediateCatchEvent; import org.flowable.bpmn.model.Signal; import org.flowable.bpmn.model.SignalEventDefinition; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.bpmn.model.ThrowEvent; import org.flowable.engine.impl.bpmn.parser.BpmnParse; /** @@ -40,6 +43,19 @@ protected void executeParse(BpmnParse bpmnParse, SignalEventDefinition signalDef } else if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundarySignalEventActivityBehavior(boundaryEvent, signalDefinition, signal, boundaryEvent.isCancelActivity())); + + } else if (bpmnParse.getCurrentFlowElement() instanceof ThrowEvent throwEvent) { + throwEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createIntermediateThrowSignalEventActivityBehavior(throwEvent, signalDefinition, signal)); + + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent) { + if (startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessSignalStartEventActivityBehavior(startEvent, signalDefinition, signal)); + } else { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createSignalStartEventActivityBehavior(startEvent, signalDefinition, signal)); + } } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/StartEventParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/StartEventParseHandler.java index 69074187b77..79c49d00146 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/StartEventParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/StartEventParseHandler.java @@ -12,26 +12,15 @@ */ package org.flowable.engine.impl.bpmn.parser.handler; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.BaseElement; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.ErrorEventDefinition; -import org.flowable.bpmn.model.EscalationEventDefinition; import org.flowable.bpmn.model.EventDefinition; +import org.flowable.bpmn.model.EventDefinitionLocation; import org.flowable.bpmn.model.EventSubProcess; -import org.flowable.bpmn.model.ExtensionElement; -import org.flowable.bpmn.model.Message; -import org.flowable.bpmn.model.MessageEventDefinition; -import org.flowable.bpmn.model.Signal; -import org.flowable.bpmn.model.SignalEventDefinition; import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.TimerEventDefinition; -import org.flowable.bpmn.model.VariableListenerEventDefinition; import org.flowable.common.engine.impl.util.CollectionUtil; import org.flowable.engine.impl.bpmn.parser.BpmnParse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Joram Barrez @@ -39,6 +28,8 @@ */ public class StartEventParseHandler extends AbstractActivityBpmnParseHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(StartEventParseHandler.class); + @Override public Class getHandledType() { return StartEvent.class; @@ -46,82 +37,34 @@ public Class getHandledType() { @Override protected void executeParse(BpmnParse bpmnParse, StartEvent element) { - if (element.getSubProcess() != null && element.getSubProcess() instanceof EventSubProcess) { + if (element.getSubProcess() instanceof EventSubProcess) { if (CollectionUtil.isNotEmpty(element.getEventDefinitions())) { EventDefinition eventDefinition = element.getEventDefinitions().get(0); - if (eventDefinition instanceof MessageEventDefinition) { - MessageEventDefinition messageDefinition = fillMessageRef(bpmnParse, eventDefinition); - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessMessageStartEventActivityBehavior(element, messageDefinition)); - - } else if (eventDefinition instanceof SignalEventDefinition signalDefinition) { - Signal signal = bpmnParse.getBpmnModel().getSignal(signalDefinition.getSignalRef()); - - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessSignalStartEventActivityBehavior( - element, signalDefinition, signal)); - - } else if (eventDefinition instanceof TimerEventDefinition timerEventDefinition) { - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessTimerStartEventActivityBehavior( - element, timerEventDefinition)); - - } else if (eventDefinition instanceof ErrorEventDefinition) { - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessErrorStartEventActivityBehavior(element)); - - } else if (eventDefinition instanceof EscalationEventDefinition) { - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessEscalationStartEventActivityBehavior(element)); - - } else if (eventDefinition instanceof VariableListenerEventDefinition variableListenerEventDefinition) { - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessVariableListenerlStartEventActivityBehavior(element, variableListenerEventDefinition)); - } - - } else if (hasEventTypeElement(element)) { - List eventTypeElements = element.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - String eventType = eventTypeElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventType)) { - element.setBehavior(bpmnParse.getActivityBehaviorFactory().createEventSubProcessEventRegistryStartEventActivityBehavior(element, eventType)); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on event sub-process start event {}", + eventDefinition.getClass().getSimpleName(), element.getId()); + return; } + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); } } else if (CollectionUtil.isEmpty(element.getEventDefinitions())) { element.setBehavior(bpmnParse.getActivityBehaviorFactory().createNoneStartEventActivityBehavior(element)); - } else if (CollectionUtil.isNotEmpty(element.getEventDefinitions())) { + } else { EventDefinition eventDefinition = element.getEventDefinitions().get(0); - if (eventDefinition instanceof MessageEventDefinition) { - fillMessageRef(bpmnParse, eventDefinition); + if (!eventDefinition.getSupportedLocations().contains(EventDefinitionLocation.START_EVENT)) { + LOGGER.warn("EventDefinition {} is not supported on process-level start event {}", + eventDefinition.getClass().getSimpleName(), element.getId()); + } else { + bpmnParse.getBpmnParserHandlers().parseElement(bpmnParse, eventDefinition); } } - if (element.getSubProcess() == null && (hasNoEventDefinitionOrTypeElement(element) || + if (element.getSubProcess() == null && (CollectionUtil.isEmpty(element.getEventDefinitions()) || bpmnParse.getCurrentProcess().getInitialFlowElement() == null)) { bpmnParse.getCurrentProcess().setInitialFlowElement(element); } } - - protected MessageEventDefinition fillMessageRef(BpmnParse bpmnParse, EventDefinition eventDefinition) { - MessageEventDefinition messageDefinition = (MessageEventDefinition) eventDefinition; - BpmnModel bpmnModel = bpmnParse.getBpmnModel(); - String messageRef = messageDefinition.getMessageRef(); - if (bpmnModel.containsMessageId(messageRef)) { - Message message = bpmnModel.getMessage(messageRef); - messageDefinition.setMessageRef(message.getName()); - messageDefinition.setExtensionElements(message.getExtensionElements()); - } - - return messageDefinition; - } - - protected boolean hasNoEventDefinitionOrTypeElement(StartEvent element) { - return CollectionUtil.isEmpty(element.getEventDefinitions()) && !hasEventTypeElement(element); - } - - protected boolean hasEventTypeElement(StartEvent element) { - boolean foundEventTypeElement = false; - List eventTypeElements = element.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeElements != null && !eventTypeElements.isEmpty()) { - foundEventTypeElement = true; - } - - return foundEventTypeElement; - } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TerminateEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TerminateEventDefinitionParseHandler.java new file mode 100644 index 00000000000..db64bbfa98e --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TerminateEventDefinitionParseHandler.java @@ -0,0 +1,33 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.bpmn.parser.handler; + +import org.flowable.bpmn.model.BaseElement; +import org.flowable.bpmn.model.EndEvent; +import org.flowable.bpmn.model.TerminateEventDefinition; +import org.flowable.engine.impl.bpmn.parser.BpmnParse; + +public class TerminateEventDefinitionParseHandler extends AbstractBpmnParseHandler { + + @Override + public Class getHandledType() { + return TerminateEventDefinition.class; + } + + @Override + protected void executeParse(BpmnParse bpmnParse, TerminateEventDefinition eventDefinition) { + if (bpmnParse.getCurrentFlowElement() instanceof EndEvent endEvent) { + endEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createTerminateEndEventActivityBehavior(endEvent)); + } + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TimerEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TimerEventDefinitionParseHandler.java index c314f7e84c8..849abbd7b32 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TimerEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/TimerEventDefinitionParseHandler.java @@ -14,7 +14,9 @@ import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.TimerEventDefinition; import org.flowable.engine.impl.bpmn.parser.BpmnParse; @@ -38,6 +40,15 @@ protected void executeParse(BpmnParse bpmnParse, TimerEventDefinition timerEvent } else if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryTimerEventActivityBehavior(boundaryEvent, timerEventDefinition, boundaryEvent.isCancelActivity())); + + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent) { + if (startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessTimerStartEventActivityBehavior(startEvent, timerEventDefinition)); + } else { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createTimerStartEventActivityBehavior(startEvent, timerEventDefinition)); + } } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/VariableListenerEventDefinitionParseHandler.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/VariableListenerEventDefinitionParseHandler.java index e3a4252caac..ef9e5709c43 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/VariableListenerEventDefinitionParseHandler.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/parser/handler/VariableListenerEventDefinitionParseHandler.java @@ -14,7 +14,9 @@ import org.flowable.bpmn.model.BaseElement; import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.EventSubProcess; import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.VariableListenerEventDefinition; import org.flowable.engine.impl.bpmn.parser.BpmnParse; @@ -37,6 +39,11 @@ protected void executeParse(BpmnParse bpmnParse, VariableListenerEventDefinition } else if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { boundaryEvent.setBehavior(bpmnParse.getActivityBehaviorFactory().createBoundaryVariableListenerEventActivityBehavior(boundaryEvent, variableListenerEventDefinition, boundaryEvent.isCancelActivity())); + + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent + && startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(bpmnParse.getActivityBehaviorFactory() + .createEventSubProcessVariableListenerlStartEventActivityBehavior(startEvent, variableListenerEventDefinition)); } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java index 196c790d3a5..179b31c9002 100755 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java @@ -42,6 +42,10 @@ import org.apache.ibatis.transaction.managed.ManagedTransactionFactory; import org.apache.ibatis.type.JdbcType; import org.flowable.batch.service.BatchServiceConfiguration; +import org.flowable.bpmn.converter.CustomEventDefinitionXmlWriter; +import org.flowable.bpmn.converter.child.BaseChildElementParser; +import org.flowable.bpmn.converter.util.BpmnXMLUtil; +import org.flowable.bpmn.model.EventDefinition; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate; import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher; @@ -165,10 +169,8 @@ import org.flowable.engine.impl.bpmn.deployer.BpmnDeployer; import org.flowable.engine.impl.bpmn.deployer.BpmnDeploymentHelper; import org.flowable.engine.impl.bpmn.deployer.CachingAndArtifactsManager; -import org.flowable.engine.impl.bpmn.deployer.EventSubscriptionManager; import org.flowable.engine.impl.bpmn.deployer.ParsedDeploymentBuilderFactory; import org.flowable.engine.impl.bpmn.deployer.ProcessDefinitionDiagramHelper; -import org.flowable.engine.impl.bpmn.deployer.TimerManager; import org.flowable.engine.impl.bpmn.listener.ListenerNotificationHelper; import org.flowable.engine.impl.bpmn.parser.BpmnParseHandlers; import org.flowable.engine.impl.bpmn.parser.BpmnParser; @@ -191,6 +193,7 @@ import org.flowable.engine.impl.bpmn.parser.handler.ErrorEventDefinitionParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.EscalationEventDefinitionParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.EventBasedGatewayParseHandler; +import org.flowable.engine.impl.bpmn.parser.handler.EventRegistryEventDefinitionParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.EventSubProcessParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.ExclusiveGatewayParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.ExternalWorkerServiceTaskParseHandler; @@ -206,6 +209,7 @@ import org.flowable.engine.impl.bpmn.parser.handler.ReceiveTaskParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.ScriptTaskParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.SendEventServiceTaskParseHandler; +import org.flowable.engine.impl.bpmn.parser.handler.TerminateEventDefinitionParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.SendTaskParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.SequenceFlowParseHandler; import org.flowable.engine.impl.bpmn.parser.handler.ServiceTaskParseHandler; @@ -561,8 +565,6 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig protected AppDeployer appDeployer; protected BpmnParser bpmnParser; protected ParsedDeploymentBuilderFactory parsedDeploymentBuilderFactory; - protected TimerManager timerManager; - protected EventSubscriptionManager eventSubscriptionManager; protected BpmnDeploymentHelper bpmnDeploymentHelper; protected CachingAndArtifactsManager cachingAndArtifactsManager; protected ProcessDefinitionDiagramHelper processDefinitionDiagramHelper; @@ -670,6 +672,17 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig protected List preBpmnParseHandlers; protected List postBpmnParseHandlers; protected List customDefaultBpmnParseHandlers; + /** + * BPMN child-element XML parsers contributed by integrators (e.g. for custom {@code EventDefinition} + * subtypes). Registered with {@link BpmnXMLUtil#addChildElementParser(BaseChildElementParser)} during engine init. + */ + protected List customChildElementParsers; + /** + * BPMN XML writers for {@link org.flowable.bpmn.model.CustomBpmnEventDefinition} subclasses contributed by + * integrators. The write-side counterpart to {@link #customChildElementParsers}; registered with + * {@link BpmnXMLUtil#addCustomEventDefinitionWriter} during engine init. + */ + protected Map, CustomEventDefinitionXmlWriter> customEventDefinitionWriters; protected ActivityBehaviorFactory activityBehaviorFactory; protected ListenerFactory listenerFactory; protected BpmnParseFactory bpmnParseFactory; @@ -1727,23 +1740,9 @@ public void initBpmnDeployerDependencies() { parsedDeploymentBuilderFactory.setBpmnParser(bpmnParser); } - if (timerManager == null) { - timerManager = new TimerManager(); - } - - if (eventSubscriptionManager == null) { - eventSubscriptionManager = new EventSubscriptionManager(); - } - if (bpmnDeploymentHelper == null) { bpmnDeploymentHelper = new BpmnDeploymentHelper(); } - if (bpmnDeploymentHelper.getTimerManager() == null) { - bpmnDeploymentHelper.setTimerManager(timerManager); - } - if (bpmnDeploymentHelper.getEventSubscriptionManager() == null) { - bpmnDeploymentHelper.setEventSubscriptionManager(eventSubscriptionManager); - } if (cachingAndArtifactsManager == null) { cachingAndArtifactsManager = new CachingAndArtifactsManager(); @@ -1817,6 +1816,18 @@ public void initBpmnParser() { bpmnParseFactory = new DefaultBpmnParseFactory(); } + if (customChildElementParsers != null) { + for (BaseChildElementParser parser : customChildElementParsers) { + BpmnXMLUtil.addChildElementParser(parser); + } + } + + if (customEventDefinitionWriters != null) { + for (Map.Entry, CustomEventDefinitionXmlWriter> entry : customEventDefinitionWriters.entrySet()) { + BpmnXMLUtil.addCustomEventDefinitionWriter(entry.getKey(), entry.getValue()); + } + } + bpmnParser.setBpmnParseFactory(bpmnParseFactory); bpmnParser.setActivityBehaviorFactory(activityBehaviorFactory); bpmnParser.setListenerFactory(listenerFactory); @@ -1850,6 +1861,7 @@ public List getDefaultBpmnParseHandlers() { bpmnParserHandlers.add(new ErrorEventDefinitionParseHandler()); bpmnParserHandlers.add(new EscalationEventDefinitionParseHandler()); bpmnParserHandlers.add(new EventBasedGatewayParseHandler()); + bpmnParserHandlers.add(new EventRegistryEventDefinitionParseHandler()); bpmnParserHandlers.add(new ExclusiveGatewayParseHandler()); bpmnParserHandlers.add(new InclusiveGatewayParseHandler()); bpmnParserHandlers.add(new IntermediateCatchEventParseHandler()); @@ -1861,6 +1873,7 @@ public List getDefaultBpmnParseHandlers() { bpmnParserHandlers.add(new ReceiveTaskParseHandler()); bpmnParserHandlers.add(new ScriptTaskParseHandler()); bpmnParserHandlers.add(new SendEventServiceTaskParseHandler()); + bpmnParserHandlers.add(new TerminateEventDefinitionParseHandler()); bpmnParserHandlers.add(new ExternalWorkerServiceTaskParseHandler()); bpmnParserHandlers.add(new SendTaskParseHandler()); bpmnParserHandlers.add(new SequenceFlowParseHandler()); @@ -2899,22 +2912,6 @@ public ProcessEngineConfigurationImpl setParsedDeploymentBuilderFactory(ParsedDe return this; } - public TimerManager getTimerManager() { - return timerManager; - } - - public void setTimerManager(TimerManager timerManager) { - this.timerManager = timerManager; - } - - public EventSubscriptionManager getEventSubscriptionManager() { - return eventSubscriptionManager; - } - - public void setEventSubscriptionManager(EventSubscriptionManager eventSubscriptionManager) { - this.eventSubscriptionManager = eventSubscriptionManager; - } - public BpmnDeploymentHelper getBpmnDeploymentHelper() { return bpmnDeploymentHelper; } @@ -3687,6 +3684,40 @@ public ProcessEngineConfigurationImpl setPostBpmnParseHandlers(List getCustomChildElementParsers() { + return customChildElementParsers; + } + + public ProcessEngineConfigurationImpl setCustomChildElementParsers(List customChildElementParsers) { + this.customChildElementParsers = customChildElementParsers; + return this; + } + + public ProcessEngineConfigurationImpl addCustomChildElementParser(BaseChildElementParser parser) { + if (customChildElementParsers == null) { + customChildElementParsers = new ArrayList<>(); + } + customChildElementParsers.add(parser); + return this; + } + + public Map, CustomEventDefinitionXmlWriter> getCustomEventDefinitionWriters() { + return customEventDefinitionWriters; + } + + public ProcessEngineConfigurationImpl setCustomEventDefinitionWriters(Map, CustomEventDefinitionXmlWriter> customEventDefinitionWriters) { + this.customEventDefinitionWriters = customEventDefinitionWriters; + return this; + } + + public ProcessEngineConfigurationImpl addCustomEventDefinitionWriter(Class eventDefinitionClass, CustomEventDefinitionXmlWriter writer) { + if (customEventDefinitionWriters == null) { + customEventDefinitionWriters = new HashMap<>(); + } + customEventDefinitionWriters.put(eventDefinitionClass, writer); + return this; + } + public ActivityBehaviorFactory getActivityBehaviorFactory() { return activityBehaviorFactory; } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/DeleteProcessInstanceStartEventSubscriptionCmd.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/DeleteProcessInstanceStartEventSubscriptionCmd.java index 58ed66a3dd8..d56519c63ae 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/DeleteProcessInstanceStartEventSubscriptionCmd.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/DeleteProcessInstanceStartEventSubscriptionCmd.java @@ -15,12 +15,15 @@ import java.io.Serializable; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.StartEvent; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.event.EventRegistryEventDefinitionUtil; import org.flowable.engine.impl.runtime.ProcessInstanceStartEventSubscriptionDeletionBuilderImpl; /** @@ -44,14 +47,14 @@ public Void execute(CommandContext commandContext) { List startEvents = process.findFlowElementsOfType(StartEvent.class, false); for (StartEvent startEvent : startEvents) { // looking for a start event based on an event-registry event subscription - List eventTypeElements = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeElements != null && eventTypeElements.size() > 0) { + EventRegistryEventDefinition eventDefinition = EventRegistryEventDefinitionUtil.findOn(startEvent); + if (eventDefinition != null && StringUtils.isNotEmpty(eventDefinition.getEventDefinitionKey())) { // looking for a dynamic, manually subscribed behavior of the event-registry start event List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); if (correlationConfiguration != null && correlationConfiguration.size() > 0 && BpmnXMLConstants.START_EVENT_CORRELATION_MANUAL.equals(correlationConfiguration.get(0).getElementText())) { - String eventDefinitionKey = eventTypeElements.get(0).getElementText(); + String eventDefinitionKey = eventDefinition.getEventDefinitionKey(); String correlationKey = null; if (builder.hasCorrelationParameterValues()) { diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/ModifyProcessInstanceStartEventSubscriptionCmd.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/ModifyProcessInstanceStartEventSubscriptionCmd.java index 72ccd95ec7a..8a68fa16eb0 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/ModifyProcessInstanceStartEventSubscriptionCmd.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/ModifyProcessInstanceStartEventSubscriptionCmd.java @@ -15,13 +15,16 @@ import java.io.Serializable; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.StartEvent; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.impl.event.EventRegistryEventDefinitionUtil; import org.flowable.engine.impl.runtime.ProcessInstanceStartEventSubscriptionModificationBuilderImpl; import org.flowable.engine.repository.ProcessDefinition; @@ -62,14 +65,14 @@ public Void execute(CommandContext commandContext) { List startEvents = process.findFlowElementsOfType(StartEvent.class, false); for (StartEvent startEvent : startEvents) { // looking for a start event based on an event-registry event subscription - List eventTypeElements = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeElements != null && eventTypeElements.size() > 0) { + EventRegistryEventDefinition eventDefinition = EventRegistryEventDefinitionUtil.findOn(startEvent); + if (eventDefinition != null && StringUtils.isNotEmpty(eventDefinition.getEventDefinitionKey())) { // looking for a dynamic, manually subscribed behavior of the event-registry start event List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); if (correlationConfiguration != null && correlationConfiguration.size() > 0 && BpmnXMLConstants.START_EVENT_CORRELATION_MANUAL.equals(correlationConfiguration.get(0).getElementText())) { - String eventDefinitionKey = eventTypeElements.get(0).getElementText(); + String eventDefinitionKey = eventDefinition.getEventDefinitionKey(); String correlationKey = null; if (builder.hasCorrelationParameterValues()) { diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/RegisterProcessInstanceStartEventSubscriptionCmd.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/RegisterProcessInstanceStartEventSubscriptionCmd.java index ce92705a737..7ae766569e8 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/RegisterProcessInstanceStartEventSubscriptionCmd.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/RegisterProcessInstanceStartEventSubscriptionCmd.java @@ -15,7 +15,9 @@ import java.io.Serializable; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.constants.BpmnXMLConstants; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.StartEvent; @@ -24,6 +26,7 @@ import org.flowable.common.engine.impl.interceptor.Command; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.event.EventRegistryEventDefinitionUtil; import org.flowable.engine.impl.runtime.ProcessInstanceStartEventSubscriptionBuilderImpl; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.impl.util.CountingEntityUtil; @@ -56,8 +59,8 @@ public EventSubscription execute(CommandContext commandContext) { List startEvents = process.findFlowElementsOfType(StartEvent.class, false); for (StartEvent startEvent : startEvents) { // looking for a start event based on an event-registry event subscription - List eventTypeElements = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeElements != null && eventTypeElements.size() > 0) { + EventRegistryEventDefinition eventDefinition = EventRegistryEventDefinitionUtil.findOn(startEvent); + if (eventDefinition != null && StringUtils.isNotEmpty(eventDefinition.getEventDefinitionKey())) { // looking for a dynamic, manually subscribed behavior of the event-registry start event List correlationConfiguration = startEvent.getExtensionElements().get(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION); if (correlationConfiguration != null && correlationConfiguration.size() > 0 && @@ -69,8 +72,8 @@ public EventSubscription execute(CommandContext commandContext) { + " has more than one event-registry start events based on manually registered subscriptions, which is currently not supported."); } - String eventDefinitionKey = eventTypeElements.get(0).getElementText(); - String correlationKey = generateCorrelationConfiguration(eventDefinitionKey, builder.getTenantId(), + String eventDefinitionKey = eventDefinition.getEventDefinitionKey(); + String correlationKey = generateCorrelationConfiguration(eventDefinitionKey, builder.getTenantId(), builder.getCorrelationParameterValues(), commandContext); eventSubscription = insertEventRegistryEvent(eventDefinitionKey, builder.isDoNotUpdateToLatestVersionAutomatically(), startEvent, processDefinition, diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java index 156a1670326..67089a4d3fd 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java @@ -29,15 +29,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.Activity; import org.flowable.bpmn.model.BoundaryEvent; import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.CallActivity; import org.flowable.bpmn.model.CompensateEventDefinition; import org.flowable.bpmn.model.EventDefinition; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.EventSubProcess; -import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.ExternalWorkerServiceTask; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.FlowElementsContainer; @@ -523,18 +522,10 @@ protected void doMoveExecutionState(ProcessInstanceChangeState processInstanceCh boolean hasEventSubscriptions = false; if (boundaryEvent.getEventDefinitions() != null && !boundaryEvent.getEventDefinitions().isEmpty()) { EventDefinition sourceEventDef = boundaryEvent.getEventDefinitions().get(0); - if (sourceEventDef instanceof SignalEventDefinition || sourceEventDef instanceof MessageEventDefinition) { + if (sourceEventDef instanceof SignalEventDefinition || sourceEventDef instanceof MessageEventDefinition + || sourceEventDef instanceof EventRegistryEventDefinition) { hasEventSubscriptions = true; } - - } else if (!boundaryEvent.getExtensionElements().isEmpty()) { - List sourceEventTypeExtensionElements = boundaryEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (sourceEventTypeExtensionElements != null && !sourceEventTypeExtensionElements.isEmpty()) { - String sourceEventTypeValue = sourceEventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(sourceEventTypeValue)) { - hasEventSubscriptions = true; - } - } } List eventSubscriptions = null; @@ -1262,18 +1253,10 @@ protected ExecutionEntity migrateExecutionEntity(ExecutionEntity parentExecution boolean hasEventSubscriptions = false; if (boundaryEvent.getEventDefinitions() != null && !boundaryEvent.getEventDefinitions().isEmpty()) { EventDefinition sourceEventDef = boundaryEvent.getEventDefinitions().get(0); - if (sourceEventDef instanceof SignalEventDefinition || sourceEventDef instanceof MessageEventDefinition) { + if (sourceEventDef instanceof SignalEventDefinition || sourceEventDef instanceof MessageEventDefinition + || sourceEventDef instanceof EventRegistryEventDefinition) { hasEventSubscriptions = true; } - - } else if (!boundaryEvent.getExtensionElements().isEmpty()) { - List sourceEventTypeExtensionElements = boundaryEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (sourceEventTypeExtensionElements != null && !sourceEventTypeExtensionElements.isEmpty()) { - String sourceEventTypeValue = sourceEventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(sourceEventTypeValue)) { - hasEventSubscriptions = true; - } - } } if (hasEventSubscriptions) { @@ -1412,23 +1395,9 @@ protected List createBoundaryEvents(List boundar for (BoundaryEvent boundaryEvent : boundaryEvents) { if (CollectionUtil.isEmpty(boundaryEvent.getEventDefinitions())) { - - boolean hasEventRegistryBoundaryEvent = false; - if (!boundaryEvent.getExtensionElements().isEmpty()) { - List eventTypeExtensionElements = boundaryEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypeExtensionElements != null && !eventTypeExtensionElements.isEmpty()) { - String eventTypeValue = eventTypeExtensionElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventTypeValue)) { - hasEventRegistryBoundaryEvent = true; - } - } - } - - if (!hasEventRegistryBoundaryEvent) { - continue; - } - - } else if (boundaryEvent.getEventDefinitions().get(0) instanceof CompensateEventDefinition) { + continue; + } + if (boundaryEvent.getEventDefinitions().get(0) instanceof CompensateEventDefinition) { continue; } @@ -1721,25 +1690,12 @@ protected boolean sameBoundaryEventDefinition(BoundaryEvent sourceEvent, Boundar if (StringUtils.isNotEmpty(timerSourceDef.getTimeDuration()) && timerSourceDef.getTimeDuration().equals(timerTargetDef.getTimeDuration())) { return true; } - } - - } else if (!sourceEvent.getExtensionElements().isEmpty() && !targetEvent.getExtensionElements().isEmpty()) { - List sourceEventTypeExtensionElements = sourceEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - List targetEventTypeExtensionElements = targetEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - String sourceEventTypeValue = null; - if (sourceEventTypeExtensionElements != null && !sourceEventTypeExtensionElements.isEmpty()) { - sourceEventTypeValue = sourceEventTypeExtensionElements.get(0).getElementText(); - } - - String targetEventTypeValue = null; - if (targetEventTypeExtensionElements != null && !targetEventTypeExtensionElements.isEmpty()) { - targetEventTypeValue = targetEventTypeExtensionElements.get(0).getElementText(); - } - - if (StringUtils.isNotEmpty(sourceEventTypeValue) && StringUtils.isNotEmpty(targetEventTypeValue) && - sourceEventTypeValue.equals(targetEventTypeValue)) { - - return true; + } else if (sourceEventDef instanceof EventRegistryEventDefinition sourceRegistry) { + EventRegistryEventDefinition targetRegistry = (EventRegistryEventDefinition) targetEventDef; + if (StringUtils.isNotEmpty(sourceRegistry.getEventDefinitionKey()) + && sourceRegistry.getEventDefinitionKey().equals(targetRegistry.getEventDefinitionKey())) { + return true; + } } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/event/EventRegistryEventDefinitionUtil.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/event/EventRegistryEventDefinitionUtil.java new file mode 100644 index 00000000000..e168d64a4b6 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/event/EventRegistryEventDefinitionUtil.java @@ -0,0 +1,38 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.event; + +import org.flowable.bpmn.model.Event; +import org.flowable.bpmn.model.EventDefinition; +import org.flowable.bpmn.model.EventRegistryEventDefinition; + +public final class EventRegistryEventDefinitionUtil { + + private EventRegistryEventDefinitionUtil() { + } + + /** + * Returns the first {@link EventRegistryEventDefinition} on the given event, or {@code null} if none. + */ + public static EventRegistryEventDefinition findOn(Event event) { + if (event == null || event.getEventDefinitions() == null) { + return null; + } + for (EventDefinition def : event.getEventDefinitions()) { + if (def instanceof EventRegistryEventDefinition eventRegistry) { + return eventRegistry; + } + } + return null; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/eventregistry/BpmnEventRegistryEventConsumer.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/eventregistry/BpmnEventRegistryEventConsumer.java index eb862d2ae12..c628e7cb977 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/eventregistry/BpmnEventRegistryEventConsumer.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/eventregistry/BpmnEventRegistryEventConsumer.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EventRegistryEventDefinition; import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.StartEvent; import org.flowable.common.engine.api.constant.ReferenceTypes; @@ -30,6 +31,7 @@ import org.flowable.engine.ProcessEngineConfiguration; import org.flowable.engine.RuntimeService; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.event.EventRegistryEventDefinitionUtil; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstanceBuilder; @@ -278,9 +280,8 @@ protected String getStartCorrelationConfiguration(EventSubscription eventSubscri List startEvents = bpmnModel.getMainProcess().findFlowElementsOfType(StartEvent.class); for (StartEvent startEvent : startEvents) { - List eventTypes = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (eventTypes != null && !eventTypes.isEmpty() - && Objects.equals(eventSubscription.getEventType(), eventTypes.get(0).getElementText())) { + EventRegistryEventDefinition eventDefinition = EventRegistryEventDefinitionUtil.findOn(startEvent); + if (eventDefinition != null && Objects.equals(eventSubscription.getEventType(), eventDefinition.getEventDefinitionKey())) { List correlationCfgExtensions = startEvent.getExtensionElements() .getOrDefault(BpmnXMLConstants.START_EVENT_CORRELATION_CONFIGURATION, Collections.emptyList()); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/repository/DeploymentProcessDefinitionDeletionManagerImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/repository/DeploymentProcessDefinitionDeletionManagerImpl.java index ea3a7c975de..43f7e65ea38 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/repository/DeploymentProcessDefinitionDeletionManagerImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/repository/DeploymentProcessDefinitionDeletionManagerImpl.java @@ -14,40 +14,25 @@ import java.util.List; -import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.EventDefinition; -import org.flowable.bpmn.model.ExtensionElement; -import org.flowable.bpmn.model.Message; -import org.flowable.bpmn.model.MessageEventDefinition; -import org.flowable.bpmn.model.SignalEventDefinition; +import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.TimerEventDefinition; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher; -import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.common.engine.impl.util.CollectionUtil; import org.flowable.engine.ProcessEngineConfiguration; import org.flowable.engine.delegate.event.impl.FlowableEventBuilder; import org.flowable.engine.impl.ProcessDefinitionQueryImpl; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventDeployContext; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.context.Context; -import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; -import org.flowable.engine.impl.jobexecutor.TimerEventHandler; import org.flowable.engine.impl.jobexecutor.TimerStartEventJobHandler; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityManager; -import org.flowable.engine.impl.util.CorrelationUtil; -import org.flowable.engine.impl.util.CountingEntityUtil; import org.flowable.engine.impl.util.ProcessDefinitionUtil; -import org.flowable.engine.impl.util.TimerUtil; import org.flowable.engine.repository.ProcessDefinition; -import org.flowable.eventsubscription.api.EventSubscription; -import org.flowable.eventsubscription.api.EventSubscriptionBuilder; -import org.flowable.eventsubscription.service.EventSubscriptionService; -import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; -import org.flowable.eventsubscription.service.impl.persistence.entity.SignalEventSubscriptionEntity; import org.flowable.job.service.TimerJobService; import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; @@ -70,10 +55,8 @@ public void deleteDefinitionForDeployment(ProcessDefinition processDefinition, S removeTimerStartJobs(processDefinition); - // If previous process definition version has a timer/signal/message start event, it must be added - // Only if the currently deleted process definition is the latest version, - // we fall back to the previous timer/signal/message start event - + // If the deleted process definition was the latest version, restore the previous version's + // timer / message / signal / event-registry start events. restorePreviousStartEventsIfNeeded(processDefinition); } @@ -108,116 +91,31 @@ protected void removeTimerStartJobs(ProcessDefinition processDefinition) { protected void restorePreviousStartEventsIfNeeded(ProcessDefinition processDefinition) { ProcessDefinitionEntity latestProcessDefinition = findLatestProcessDefinition(processDefinition); - if (latestProcessDefinition != null && processDefinition.getId().equals(latestProcessDefinition.getId())) { - - // Try to find a previous version (it could be some versions are missing due to deletions) - ProcessDefinition previousProcessDefinition = findNewLatestProcessDefinitionAfterRemovalOf(processDefinition); - if (previousProcessDefinition != null) { - - BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(previousProcessDefinition.getId()); - org.flowable.bpmn.model.Process previousProcess = ProcessDefinitionUtil.getProcess(previousProcessDefinition.getId()); - if (CollectionUtil.isNotEmpty(previousProcess.getFlowElements())) { - - List startEvents = previousProcess.findFlowElementsOfType(StartEvent.class); - - if (CollectionUtil.isNotEmpty(startEvents)) { - for (StartEvent startEvent : startEvents) { - - if (CollectionUtil.isNotEmpty(startEvent.getEventDefinitions())) { - EventDefinition eventDefinition = startEvent.getEventDefinitions().get(0); - if (eventDefinition instanceof TimerEventDefinition) { - restoreTimerStartEvent(previousProcessDefinition, startEvent, eventDefinition); - } else if (eventDefinition instanceof SignalEventDefinition) { - restoreSignalStartEvent(previousProcessDefinition, bpmnModel, startEvent, eventDefinition); - } else if (eventDefinition instanceof MessageEventDefinition) { - restoreMessageStartEvent(previousProcessDefinition, bpmnModel, startEvent, eventDefinition); - } - - } else { - if (startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE) != null) { - List eventTypeElements = startEvent.getExtensionElements().get(BpmnXMLConstants.ELEMENT_EVENT_TYPE); - if (!eventTypeElements.isEmpty()) { - String eventDefinitionKey = eventTypeElements.get(0).getElementText(); - restoreEventRegistryStartEvent(previousProcessDefinition, bpmnModel, startEvent, eventDefinitionKey); - } - } - } - - } - } - - } - - } - } - } - - protected void restoreTimerStartEvent(ProcessDefinition previousProcessDefinition, StartEvent startEvent, EventDefinition eventDefinition) { - TimerEventDefinition timerEventDefinition = (TimerEventDefinition) eventDefinition; - - TimerJobEntity timerJob = TimerUtil.createTimerEntityForTimerEventDefinition(timerEventDefinition, startEvent, - false, previousProcessDefinition, TimerStartEventJobHandler.TYPE, TimerEventHandler.createConfiguration(startEvent.getId(), - timerEventDefinition.getEndDate(), timerEventDefinition.getCalendarName())); - - engineConfiguration.getJobServiceConfiguration().getTimerJobService().scheduleTimerJob(timerJob); - } - - protected void restoreSignalStartEvent(ProcessDefinition previousProcessDefinition, BpmnModel bpmnModel, StartEvent startEvent, EventDefinition eventDefinition) { - CommandContext commandContext = Context.getCommandContext(); - SignalEventDefinition signalEventDefinition = (SignalEventDefinition) eventDefinition; - SignalEventSubscriptionEntity subscriptionEntity = engineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService().createSignalEventSubscription(); - - String eventName = EventDefinitionExpressionUtil.determineSignalName(commandContext, signalEventDefinition, bpmnModel, previousProcessDefinition); - subscriptionEntity.setEventName(eventName); - subscriptionEntity.setActivityId(startEvent.getId()); - subscriptionEntity.setProcessDefinitionId(previousProcessDefinition.getId()); - if (previousProcessDefinition.getTenantId() != null) { - subscriptionEntity.setTenantId(previousProcessDefinition.getTenantId()); + if (latestProcessDefinition == null || !processDefinition.getId().equals(latestProcessDefinition.getId())) { + return; } - engineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService().insertEventSubscription(subscriptionEntity); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(subscriptionEntity); - } - - protected void restoreMessageStartEvent(ProcessDefinition previousProcessDefinition, BpmnModel bpmnModel, StartEvent startEvent, EventDefinition eventDefinition) { - MessageEventDefinition messageEventDefinition = (MessageEventDefinition) eventDefinition; - if (bpmnModel.containsMessageId(messageEventDefinition.getMessageRef())) { - Message message = bpmnModel.getMessage(messageEventDefinition.getMessageRef()); - messageEventDefinition.setMessageRef(message.getName()); + // Try to find a previous version (it could be some versions are missing due to deletions) + ProcessDefinition previousProcessDefinition = findNewLatestProcessDefinitionAfterRemovalOf(processDefinition); + if (previousProcessDefinition == null) { + return; } - CommandContext commandContext = Context.getCommandContext(); - MessageEventSubscriptionEntity newSubscription = engineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService().createMessageEventSubscription(); - String messageName = EventDefinitionExpressionUtil.determineMessageName(commandContext, messageEventDefinition, previousProcessDefinition); - newSubscription.setEventName(messageName); - newSubscription.setActivityId(startEvent.getId()); - newSubscription.setConfiguration(previousProcessDefinition.getId()); - newSubscription.setProcessDefinitionId(previousProcessDefinition.getId()); - - if (previousProcessDefinition.getTenantId() != null) { - newSubscription.setTenantId(previousProcessDefinition.getTenantId()); + org.flowable.bpmn.model.Process previousProcess = ProcessDefinitionUtil.getProcess(previousProcessDefinition.getId()); + if (CollectionUtil.isEmpty(previousProcess.getFlowElements())) { + return; } - engineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService().insertEventSubscription(newSubscription); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(newSubscription); - } - - protected void restoreEventRegistryStartEvent(ProcessDefinition previousProcessDefinition, BpmnModel bpmnModel, StartEvent startEvent, String eventDefinitionKey) { + BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(previousProcessDefinition.getId()); CommandContext commandContext = Context.getCommandContext(); - EventSubscriptionService eventSubscriptionService = engineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); - EventSubscriptionBuilder eventSubscriptionBuilder = eventSubscriptionService.createEventSubscriptionBuilder() - .eventType(eventDefinitionKey) - .activityId(startEvent.getId()) - .processDefinitionId(previousProcessDefinition.getId()) - .scopeType(ScopeTypes.BPMN) - .configuration(CorrelationUtil.getCorrelationKey(BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, commandContext, startEvent, null)); - - if (previousProcessDefinition.getTenantId() != null) { - eventSubscriptionBuilder.tenantId(previousProcessDefinition.getTenantId()); + ProcessDefinitionEntity previousProcessDefinitionEntity = (ProcessDefinitionEntity) previousProcessDefinition; + for (FlowElement flowElement : previousProcess.getFlowElements()) { + if (flowElement instanceof StartEvent startEvent + && startEvent.getBehavior() instanceof ProcessLevelStartEventActivityBehavior behavior) { + behavior.deploy(new ProcessLevelStartEventDeployContext(previousProcessDefinitionEntity, + previousProcess, bpmnModel, startEvent, engineConfiguration, commandContext, true)); + } } - - EventSubscription eventSubscription = eventSubscriptionBuilder.create(); - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); } protected ProcessDefinitionEntity findLatestProcessDefinition(ProcessDefinition processDefinition) { diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java index 28005d0b47e..5320b069d82 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java @@ -20,21 +20,13 @@ import java.util.Objects; import org.apache.commons.lang3.StringUtils; -import org.flowable.bpmn.constants.BpmnXMLConstants; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.EventDefinition; import org.flowable.bpmn.model.EventSubProcess; -import org.flowable.bpmn.model.ExtensionElement; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.FlowElementsContainer; import org.flowable.bpmn.model.MessageEventDefinition; import org.flowable.bpmn.model.Process; -import org.flowable.bpmn.model.Signal; -import org.flowable.bpmn.model.SignalEventDefinition; import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.TimerEventDefinition; import org.flowable.bpmn.model.ValuedDataObject; -import org.flowable.bpmn.model.VariableListenerEventDefinition; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; @@ -43,18 +35,17 @@ import org.flowable.common.engine.impl.callback.CallbackData; import org.flowable.common.engine.impl.callback.RuntimeInstanceStateChangeCallback; import org.flowable.common.engine.impl.context.Context; -import org.flowable.common.engine.impl.el.DefinitionVariableContainer; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.common.engine.impl.logging.LoggingSessionConstants; import org.flowable.common.engine.impl.util.CollectionUtil; import org.flowable.engine.ProcessEngineConfiguration; import org.flowable.engine.compatibility.Flowable5CompatibilityHandler; import org.flowable.engine.delegate.event.impl.FlowableEventBuilder; +import org.flowable.engine.impl.bpmn.behavior.EventSubProcessStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventSubProcessStartEventInitializerContext; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; import org.flowable.engine.impl.event.EventDefinitionExpressionUtil; import org.flowable.engine.impl.eventregistry.BpmnEventInstanceOutParameterHandler; -import org.flowable.engine.impl.jobexecutor.TimerEventHandler; -import org.flowable.engine.impl.jobexecutor.TriggerTimerEventJobHandler; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityManager; import org.flowable.engine.impl.runtime.callback.ProcessInstanceState; @@ -65,12 +56,7 @@ import org.flowable.eventregistry.api.runtime.EventInstance; import org.flowable.eventregistry.impl.constant.EventConstants; import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; -import org.flowable.eventsubscription.service.impl.persistence.entity.MessageEventSubscriptionEntity; -import org.flowable.eventsubscription.service.impl.persistence.entity.SignalEventSubscriptionEntity; import org.flowable.identitylink.api.IdentityLinkType; -import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; - -import tools.jackson.databind.node.ObjectNode; /** * @author Tijs Rademakers @@ -374,171 +360,15 @@ public void processEventSubProcessStartEvent(FlowElement subElement, ExecutionEn StartEvent startEvent = (StartEvent) subElement; if (CollectionUtil.isEmpty(startEvent.getEventDefinitions())) { - List eventTypeElements = startEvent.getExtensionElements().get("eventType"); - if (eventTypeElements != null && !eventTypeElements.isEmpty()) { - String eventType = eventTypeElements.get(0).getElementText(); - if (StringUtils.isNotEmpty(eventType)) { - ExecutionEntity eventRegistryExecution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); - eventRegistryExecution.setCurrentFlowElement(startEvent); - eventRegistryExecution.setEventScope(true); - eventRegistryExecution.setActive(false); - - EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) processEngineConfiguration.getEventSubscriptionServiceConfiguration() - .getEventSubscriptionService().createEventSubscriptionBuilder() - .eventType(eventType) - .executionId(eventRegistryExecution.getId()) - .processInstanceId(eventRegistryExecution.getProcessInstanceId()) - .activityId(eventRegistryExecution.getCurrentActivityId()) - .processDefinitionId(eventRegistryExecution.getProcessDefinitionId()) - .scopeType(ScopeTypes.BPMN) - .tenantId(eventRegistryExecution.getTenantId()) - .configuration(CorrelationUtil.getCorrelationKey(BpmnXMLConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, commandContext, eventRegistryExecution)) - .create(); - - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); - } - } - return; } - EventDefinition eventDefinition = startEvent.getEventDefinitions().get(0); - if (eventDefinition instanceof MessageEventDefinition) { - handleMessageEventSubscription(eventDefinition, startEvent, parentExecution, messageEventSubscriptions, processEngineConfiguration, commandContext); - - } else if (eventDefinition instanceof SignalEventDefinition) { - handleSignalEventSubscription(eventDefinition, startEvent, parentExecution, signalEventSubscriptions, processEngineConfiguration, commandContext); - - } else if (eventDefinition instanceof TimerEventDefinition) { - handleTimerEvent(eventDefinition, startEvent, parentExecution, processEngineConfiguration); - - } else if (eventDefinition instanceof VariableListenerEventDefinition) { - handleVariableListenerEventSubscription(eventDefinition, startEvent, parentExecution, processEngineConfiguration, commandContext); - } - } - - protected void handleMessageEventSubscription(EventDefinition eventDefinition, StartEvent startEvent, ExecutionEntity parentExecution, - List messageEventSubscriptions, ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext) { - - MessageEventDefinition messageEventDefinition = (MessageEventDefinition) eventDefinition; - BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(parentExecution.getProcessDefinitionId()); - if (bpmnModel.containsMessageId(messageEventDefinition.getMessageRef())) { - messageEventDefinition.setMessageRef(bpmnModel.getMessage(messageEventDefinition.getMessageRef()).getName()); - } - - ExecutionEntity messageExecution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); - messageExecution.setCurrentFlowElement(startEvent); - messageExecution.setEventScope(true); - messageExecution.setActive(false); - - String messageName = EventDefinitionExpressionUtil.determineMessageName(commandContext, messageEventDefinition, parentExecution); - EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) processEngineConfiguration.getEventSubscriptionServiceConfiguration() - .getEventSubscriptionService().createEventSubscriptionBuilder() - .eventType(MessageEventSubscriptionEntity.EVENT_TYPE) - .eventName(messageName) - .executionId(messageExecution.getId()) - .processInstanceId(messageExecution.getProcessInstanceId()) - .activityId(messageExecution.getCurrentActivityId()) - .processDefinitionId(messageExecution.getProcessDefinitionId()) - .tenantId(messageExecution.getTenantId()) - .create(); - - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); - if (messageEventSubscriptions != null) { - messageEventSubscriptions.add(eventSubscription); - } - messageExecution.getEventSubscriptions().add(eventSubscription); - } - - protected void handleSignalEventSubscription(EventDefinition eventDefinition, StartEvent startEvent, ExecutionEntity parentExecution, - List signalEventSubscriptions, ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext) { - - SignalEventDefinition signalEventDefinition = (SignalEventDefinition) eventDefinition; - BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(parentExecution.getProcessDefinitionId()); - Signal signal = bpmnModel.getSignal(signalEventDefinition.getSignalRef()); - if (signal != null) { - signalEventDefinition.setSignalRef(signal.getName()); - } - - ExecutionEntity signalExecution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); - signalExecution.setCurrentFlowElement(startEvent); - signalExecution.setEventScope(true); - signalExecution.setActive(false); - - DefinitionVariableContainer definitionVariableContainer = new DefinitionVariableContainer(parentExecution.getProcessDefinitionId(), - parentExecution.getProcessDefinitionKey(), parentExecution.getDeploymentId(), ScopeTypes.BPMN, parentExecution.getTenantId()); - String eventName = EventDefinitionExpressionUtil.determineSignalName(commandContext, signalEventDefinition, bpmnModel, definitionVariableContainer); - - EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) processEngineConfiguration.getEventSubscriptionServiceConfiguration() - .getEventSubscriptionService().createEventSubscriptionBuilder() - .eventType(SignalEventSubscriptionEntity.EVENT_TYPE) - .eventName(eventName) - .signal(signal) - .executionId(signalExecution.getId()) - .processInstanceId(signalExecution.getProcessInstanceId()) - .activityId(signalExecution.getCurrentActivityId()) - .processDefinitionId(signalExecution.getProcessDefinitionId()) - .tenantId(signalExecution.getTenantId()) - .create(); - - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); - if (signalEventSubscriptions != null) { - signalEventSubscriptions.add(eventSubscription); - } - signalExecution.getEventSubscriptions().add(eventSubscription); - } - - protected void handleTimerEvent(EventDefinition eventDefinition, StartEvent startEvent, ExecutionEntity parentExecution, - ProcessEngineConfigurationImpl processEngineConfiguration) { - - TimerEventDefinition timerEventDefinition = (TimerEventDefinition) eventDefinition; - - ExecutionEntity timerExecution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); - timerExecution.setCurrentFlowElement(startEvent); - timerExecution.setEventScope(true); - timerExecution.setActive(false); - - TimerJobEntity timerJob = TimerUtil.createTimerEntityForTimerEventDefinition(timerEventDefinition, startEvent, - false, timerExecution, TriggerTimerEventJobHandler.TYPE, TimerEventHandler.createConfiguration(startEvent.getId(), - timerEventDefinition.getEndDate(), timerEventDefinition.getCalendarName())); - - if (timerJob != null) { - processEngineConfiguration.getJobServiceConfiguration().getTimerJobService().scheduleTimerJob(timerJob); + if (startEvent.getBehavior() instanceof EventSubProcessStartEventActivityBehavior behavior) { + behavior.initializeEventSubProcessStart(new EventSubProcessStartEventInitializerContext( + parentExecution, startEvent, processEngineConfiguration, commandContext, + messageEventSubscriptions, signalEventSubscriptions)); } } - - protected void handleVariableListenerEventSubscription(EventDefinition eventDefinition, StartEvent startEvent, ExecutionEntity parentExecution, - ProcessEngineConfigurationImpl processEngineConfiguration, CommandContext commandContext) { - - VariableListenerEventDefinition variableListenerEventDefinition = (VariableListenerEventDefinition) eventDefinition; - - ExecutionEntity variableListenerExecution = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parentExecution); - variableListenerExecution.setCurrentFlowElement(startEvent); - variableListenerExecution.setEventScope(true); - variableListenerExecution.setActive(false); - - String configuration = null; - if (StringUtils.isNotEmpty(variableListenerEventDefinition.getVariableChangeType())) { - ObjectNode configurationNode = processEngineConfiguration.getObjectMapper().createObjectNode(); - configurationNode.put(VariableListenerEventDefinition.CHANGE_TYPE_PROPERTY, variableListenerEventDefinition.getVariableChangeType()); - configuration = configurationNode.toString(); - } - - EventSubscriptionEntity eventSubscription = (EventSubscriptionEntity) processEngineConfiguration.getEventSubscriptionServiceConfiguration() - .getEventSubscriptionService().createEventSubscriptionBuilder() - .eventType("variable") - .eventName(variableListenerEventDefinition.getVariableName()) - .configuration(configuration) - .executionId(variableListenerExecution.getId()) - .processInstanceId(variableListenerExecution.getProcessInstanceId()) - .activityId(variableListenerExecution.getCurrentActivityId()) - .processDefinitionId(variableListenerExecution.getProcessDefinitionId()) - .tenantId(variableListenerExecution.getTenantId()) - .create(); - - CountingEntityUtil.handleInsertEventSubscriptionEntityCount(eventSubscription); - variableListenerExecution.getEventSubscriptions().add(eventSubscription); - } protected Map processDataObjects(Collection dataObjects) { Map variablesMap = new HashMap<>(); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/test/TestActivityBehaviorFactory.java b/modules/flowable-engine/src/main/java/org/flowable/engine/test/TestActivityBehaviorFactory.java index 83ed0523320..3af4af8ab53 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/test/TestActivityBehaviorFactory.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/test/TestActivityBehaviorFactory.java @@ -101,7 +101,11 @@ import org.flowable.engine.impl.bpmn.behavior.IntermediateThrowSignalEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ManualTaskActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneEndEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventRegistryStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.MessageStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.NoneStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.SignalStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.TimerStartEventActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelGatewayActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.ReceiveEventTaskActivityBehavior; @@ -162,6 +166,26 @@ public NoneStartEventActivityBehavior createNoneStartEventActivityBehavior(Start return wrappedActivityBehaviorFactory.createNoneStartEventActivityBehavior(startEvent); } + @Override + public MessageStartEventActivityBehavior createMessageStartEventActivityBehavior(StartEvent startEvent, MessageEventDefinition messageEventDefinition) { + return wrappedActivityBehaviorFactory.createMessageStartEventActivityBehavior(startEvent, messageEventDefinition); + } + + @Override + public SignalStartEventActivityBehavior createSignalStartEventActivityBehavior(StartEvent startEvent, SignalEventDefinition signalEventDefinition, Signal signal) { + return wrappedActivityBehaviorFactory.createSignalStartEventActivityBehavior(startEvent, signalEventDefinition, signal); + } + + @Override + public TimerStartEventActivityBehavior createTimerStartEventActivityBehavior(StartEvent startEvent, TimerEventDefinition timerEventDefinition) { + return wrappedActivityBehaviorFactory.createTimerStartEventActivityBehavior(startEvent, timerEventDefinition); + } + + @Override + public EventRegistryStartEventActivityBehavior createEventRegistryStartEventActivityBehavior(StartEvent startEvent, String eventDefinitionKey, boolean manualCorrelation) { + return wrappedActivityBehaviorFactory.createEventRegistryStartEventActivityBehavior(startEvent, eventDefinitionKey, manualCorrelation); + } + @Override public TaskActivityBehavior createTaskActivityBehavior(Task task) { return wrappedActivityBehaviorFactory.createTaskActivityBehavior(task); diff --git a/modules/flowable-engine/src/test/java/org/flowable/standalone/parsing/CustomEventDefinitionTest.java b/modules/flowable-engine/src/test/java/org/flowable/standalone/parsing/CustomEventDefinitionTest.java new file mode 100644 index 00000000000..65228e3217b --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/standalone/parsing/CustomEventDefinitionTest.java @@ -0,0 +1,474 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.standalone.parsing; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.flowable.bpmn.converter.CustomEventDefinitionXmlWriter; +import org.flowable.bpmn.converter.child.BaseChildElementParser; +import org.flowable.bpmn.converter.util.BpmnXMLUtil; +import org.flowable.bpmn.model.BaseElement; +import org.flowable.bpmn.model.BoundaryEvent; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.CustomBpmnEventDefinition; +import org.flowable.bpmn.model.Event; +import org.flowable.bpmn.model.EventDefinition; +import org.flowable.bpmn.model.EventDefinitionLocation; +import org.flowable.bpmn.model.EventSubProcess; +import org.flowable.bpmn.model.IntermediateCatchEvent; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.engine.ProcessEngineConfiguration; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.BoundaryEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventSubProcessStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.EventSubProcessStartEventInitializerContext; +import org.flowable.engine.impl.bpmn.behavior.FlowNodeActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventDeployContext; +import org.flowable.engine.impl.bpmn.behavior.ProcessLevelStartEventUndeployContext; +import org.flowable.engine.impl.bpmn.parser.BpmnParse; +import org.flowable.engine.impl.bpmn.parser.handler.AbstractBpmnParseHandler; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.test.ResourceFlowableTestCase; +import org.flowable.common.engine.api.scope.ScopeTypes; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.test.Deployment; +import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +/** + * Verifies that a custom {@link EventDefinition} can be wired end-to-end via the public + * {@code customChildElementParsers}, {@code customEventDefinitionWriters}, and + * {@code customDefaultBpmnParseHandlers} extension points without modifying the engine source. + */ +public class CustomEventDefinitionTest extends ResourceFlowableTestCase { + + public CustomEventDefinitionTest() { + super("flowable.cfg.xml", "customEventDefinitionTest"); + } + + @Override + protected void additionalConfiguration(ProcessEngineConfiguration processEngineConfiguration) { + // customDefaultBpmnParseHandlers REPLACES handlers for matching handled types — no built-in handler + // matches MyTestEventDefinition, so use postBpmnParseHandlers (or preBpmnParseHandlers) to ADD a new + // handler instead. The validators learn the type's allowed contexts from + // MyTestEventDefinition.getSupportedLocations() — no engine-config registration needed. + ProcessEngineConfigurationImpl cfg = (ProcessEngineConfigurationImpl) processEngineConfiguration; + cfg.setCustomChildElementParsers(Collections.singletonList(new MyTestEventDefinitionParser())); + cfg.addCustomEventDefinitionWriter(MyTestEventDefinition.class, new MyTestEventDefinitionWriter()); + cfg.setPostBpmnParseHandlers(Collections.singletonList(new MyTestEventDefinitionParseHandler())); + } + + @AfterAll + static void cleanupRegistries() { + // Both setCustomChildElementParsers and addCustomEventDefinitionWriter write into JVM-static + // BpmnXMLUtil registries at engine init; closing the engine doesn't undo them. + BpmnXMLUtil.removeChildElementParser("myTestEventDefinition"); + BpmnXMLUtil.removeCustomEventDefinitionWriter(MyTestEventDefinition.class); + } + + @Test + @Deployment(resources = "org/flowable/standalone/parsing/CustomEventDefinitionTest.bpmn20.xml") + public void testCustomEventDefinitionOnIntermediateCatchEvent() { + MyTestEventCatchBehavior.RECORDED_KEYS.clear(); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("customEventDefinitionProcess"); + assertThat(MyTestEventCatchBehavior.RECORDED_KEYS).containsExactly("widget-42"); + assertThat(runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).count()).isZero(); + } + + @Test + @Deployment(resources = "org/flowable/standalone/parsing/CustomEventDefinitionTest.eventSubProcess.bpmn20.xml") + public void testCustomEventDefinitionOnEventSubProcessStartEvent() { + // Validates that a custom EventDefinition on an event-sub-process start event parses, is accepted by + // the validator, the custom parse handler installs the typed behavior, and the engine creates a + // waiting execution that can be triggered via runtimeService.trigger(...). + MyTestEventSubProcessStartBehavior.RECORDED_KEYS.clear(); + + // Start the process; the engine creates a waiting execution at the custom start event with + // eventScope=true and active=false. behavior.execute() is not invoked at this point — that mirrors + // the standard pattern shared with built-in subscription-based start events (Message, Signal, etc.): + // the behavior is invoked only on trigger. initializeEventSubProcessStart() IS invoked and records + // its key so we can assert the engine called the new EventSubProcessStartEventActivityBehavior hook. + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("customEventDefinitionEventSubProcess"); + assertThat(MyTestEventSubProcessStartBehavior.RECORDED_KEYS).containsExactly("initialize-esp-7"); + assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult().getName()) + .isEqualTo("Main task"); + + // The event-sub-process start event execution is created with active=false (standard event-scope + // pattern), so the activityId-filtered query won't match it. Iterate all executions of the instance + // to locate it. + Execution startExecution = runtimeService.createExecutionQuery() + .processInstanceId(processInstance.getId()) + .list().stream() + .filter(e -> "customEventSubProcessStart".equals(e.getActivityId())) + .findFirst().orElse(null); + assertThat(startExecution).isNotNull(); + + // Trigger the start event — the behavior records and leaves to the event sub-process body, which + // completes (no further activity) while the main user task remains. + runtimeService.trigger(startExecution.getId()); + assertThat(MyTestEventSubProcessStartBehavior.RECORDED_KEYS).containsExactly("initialize-esp-7", "trigger-esp-7"); + + // Complete the main user task; the process completes. + taskService.complete(taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult().getId()); + assertThat(runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).count()).isZero(); + } + + @Test + public void testCustomEventDefinitionOnProcessStartEvent() { + // Validates that a custom EventDefinition on a process-level start event whose ActivityBehavior + // implements ProcessLevelStartEventActivityBehavior receives deploy() at deploy time and + // undeploy() when superseded by a new version. + MyTestProcessStartBehavior.RECORDED_KEYS.clear(); + + // First deployment. + String firstDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + deploymentIdsForAutoCleanup.add(firstDeploymentId); + + // After v1 deploy: only deploy@1 is recorded (no previous process definition to undeploy). + assertThat(MyTestProcessStartBehavior.RECORDED_KEYS).containsExactly("deploy-proc-start-1@1"); + + // Redeploy the same resource — this creates v2; v1 is the previous process definition that should be undeployed. + String secondDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + deploymentIdsForAutoCleanup.add(secondDeploymentId); + + // After v2 deploy: undeploy is called for v1 BEFORE deploy is called for v2. + assertThat(MyTestProcessStartBehavior.RECORDED_KEYS) + .containsExactly("deploy-proc-start-1@1", "undeploy-proc-start-1@1", "deploy-proc-start-1@2"); + + } + + @Test + public void testCustomEventDefinitionOnProcessStartEventBulkFlushDeletesObsoleteSubscriptions() { + MyTestProcessStartBehavior.RECORDED_KEYS.clear(); + + String firstDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + deploymentIdsForAutoCleanup.add(firstDeploymentId); + String v1ProcessDefinitionId = repositoryService.createProcessDefinitionQuery() + .deploymentId(firstDeploymentId).singleResult().getId(); + + assertThat(findSubscriptions(v1ProcessDefinitionId, MyTestProcessStartBehavior.OBSOLETE_TYPE)).hasSize(1); + + String secondDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + deploymentIdsForAutoCleanup.add(secondDeploymentId); + String v2ProcessDefinitionId = repositoryService.createProcessDefinitionQuery() + .deploymentId(secondDeploymentId).singleResult().getId(); + + assertThat(findSubscriptions(v1ProcessDefinitionId, MyTestProcessStartBehavior.OBSOLETE_TYPE)).isEmpty(); + assertThat(findSubscriptions(v2ProcessDefinitionId, MyTestProcessStartBehavior.OBSOLETE_TYPE)).hasSize(1); + } + + private List findSubscriptions(String processDefinitionId, String type) { + return managementService.executeCommand(commandContext -> + ((ProcessEngineConfigurationImpl) processEngine.getProcessEngineConfiguration()) + .getEventSubscriptionServiceConfiguration().getEventSubscriptionService() + .findEventSubscriptionsByTypesAndProcessDefinitionId(Collections.singleton(type), processDefinitionId, null)); + } + + @Test + public void testCustomEventDefinitionOnProcessStartEventDeletionRestore() { + MyTestProcessStartBehavior.RECORDED_KEYS.clear(); + + String firstDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + deploymentIdsForAutoCleanup.add(firstDeploymentId); + + String secondDeploymentId = repositoryService.createDeployment() + .addClasspathResource("org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml") + .deploy().getId(); + + repositoryService.deleteDeployment(secondDeploymentId, true); + + assertThat(MyTestProcessStartBehavior.RECORDED_KEYS) + .containsExactly( + "deploy-proc-start-1@1", + "undeploy-proc-start-1@1", + "deploy-proc-start-1@2", + "deploy-proc-start-1@1[restoring]"); + } + + @Test + @Deployment(resources = "org/flowable/standalone/parsing/CustomEventDefinitionTest.boundary.bpmn20.xml") + public void testCustomEventDefinitionOnBoundaryEvent() { + MyTestBoundaryBehavior.RECORDED_KEYS.clear(); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("customEventDefinitionBoundaryProcess"); + assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult().getName()) + .isEqualTo("Wait for boundary"); + + assertThat(MyTestBoundaryBehavior.RECORDED_KEYS).containsExactly("execute-boundary-99"); + + Execution boundaryExecution = runtimeService.createExecutionQuery() + .processInstanceId(processInstance.getId()) + .activityId("customBoundary") + .singleResult(); + assertThat(boundaryExecution).isNotNull(); + runtimeService.trigger(boundaryExecution.getId()); + + assertThat(MyTestBoundaryBehavior.RECORDED_KEYS).containsExactly("execute-boundary-99", "trigger-boundary-99"); + + assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isZero(); + assertThat(runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()).count()).isZero(); + } + + // ---- Custom EventDefinition model ---- + + public static class MyTestEventDefinition extends EventDefinition implements CustomBpmnEventDefinition { + + private static final Set SUPPORTED_LOCATIONS = EnumSet.of( + EventDefinitionLocation.START_EVENT, + EventDefinitionLocation.INTERMEDIATE_CATCH_EVENT, + EventDefinitionLocation.BOUNDARY_EVENT, + EventDefinitionLocation.EVENT_SUBPROCESS_START_EVENT); + + protected String customKey; + + @Override + public Set getSupportedLocations() { + return SUPPORTED_LOCATIONS; + } + + public String getCustomKey() { + return customKey; + } + + public void setCustomKey(String customKey) { + this.customKey = customKey; + } + + @Override + public MyTestEventDefinition clone() { + MyTestEventDefinition clone = new MyTestEventDefinition(); + clone.setValues(this); + clone.setCustomKey(customKey); + return clone; + } + } + + // ---- Custom XML parser registered via customChildElementParsers ---- + + public static class MyTestEventDefinitionParser extends BaseChildElementParser { + + @Override + public String getElementName() { + return "myTestEventDefinition"; + } + + @Override + public void parseChildElement(XMLStreamReader xtr, BaseElement parentElement, BpmnModel model) { + if (!(parentElement instanceof Event event)) { + return; + } + MyTestEventDefinition definition = new MyTestEventDefinition(); + definition.setCustomKey(xtr.getAttributeValue(null, "customKey")); + event.addEventDefinition(definition); + } + } + + // ---- Custom XML writer registered via customEventDefinitionWriters; the write-side counterpart ---- + + public static class MyTestEventDefinitionWriter implements CustomEventDefinitionXmlWriter { + + @Override + public void write(Event parentEvent, EventDefinition eventDefinition, XMLStreamWriter xtw) throws Exception { + MyTestEventDefinition myTest = (MyTestEventDefinition) eventDefinition; + xtw.writeStartElement("flowable", "myTestEventDefinition", "http://flowable.org/bpmn"); + if (myTest.getCustomKey() != null) { + xtw.writeAttribute("customKey", myTest.getCustomKey()); + } + xtw.writeEndElement(); + } + } + + // ---- Custom parse handler registered via customDefaultBpmnParseHandlers ---- + + public static class MyTestEventDefinitionParseHandler extends AbstractBpmnParseHandler { + + @Override + public Class getHandledType() { + return MyTestEventDefinition.class; + } + + @Override + protected void executeParse(BpmnParse bpmnParse, MyTestEventDefinition eventDefinition) { + // The parse handler knows the typed model, so it constructs the behavior directly — no factory + // dispatch indirection needed. + if (bpmnParse.getCurrentFlowElement() instanceof IntermediateCatchEvent intermediateCatchEvent) { + intermediateCatchEvent.setBehavior(new MyTestEventCatchBehavior(eventDefinition.getCustomKey())); + } else if (bpmnParse.getCurrentFlowElement() instanceof BoundaryEvent boundaryEvent) { + boundaryEvent.setBehavior(new MyTestBoundaryBehavior(eventDefinition.getCustomKey(), boundaryEvent.isCancelActivity())); + } else if (bpmnParse.getCurrentFlowElement() instanceof StartEvent startEvent) { + if (startEvent.getSubProcess() instanceof EventSubProcess) { + startEvent.setBehavior(new MyTestEventSubProcessStartBehavior(eventDefinition.getCustomKey())); + } else { + startEvent.setBehavior(new MyTestProcessStartBehavior(eventDefinition.getCustomKey())); + } + } + } + } + + public static class MyTestEventCatchBehavior extends AbstractBpmnActivityBehavior { + + private static final long serialVersionUID = 1L; + + // Records the keys observed during execute(). Test-only side channel so we can assert the engine + // actually invoked our behavior end-to-end. + public static final List RECORDED_KEYS = new CopyOnWriteArrayList<>(); + + protected final String customKey; + + public MyTestEventCatchBehavior(String customKey) { + this.customKey = customKey; + } + + public String getCustomKey() { + return customKey; + } + + @Override + public void execute(DelegateExecution execution) { + RECORDED_KEYS.add(customKey); + leave(execution); + } + } + + public static class MyTestEventSubProcessStartBehavior extends AbstractBpmnActivityBehavior implements EventSubProcessStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + public static final List RECORDED_KEYS = new CopyOnWriteArrayList<>(); + + protected final String customKey; + + public MyTestEventSubProcessStartBehavior(String customKey) { + this.customKey = customKey; + } + + public String getCustomKey() { + return customKey; + } + + @Override + public void initializeEventSubProcessStart(EventSubProcessStartEventInitializerContext context) { + RECORDED_KEYS.add("initialize-" + customKey); + // Create the standard waiting child execution so external code can later trigger us. A real + // integration would also wire up a subscription / callback that resolves the trigger; we keep + // it simple here and just make the execution triggerable via runtimeService.trigger(...). + context.createEventScopeChildExecution(); + } + + @Override + public void execute(DelegateExecution execution) { + RECORDED_KEYS.add("execute-" + customKey); + } + + @Override + public void trigger(DelegateExecution execution, String triggerName, Object triggerData) { + RECORDED_KEYS.add("trigger-" + customKey); + // Test-only mock: full event-sub-process trigger semantics (interrupting/non-interrupting, + // sibling cleanup, scope creation) are out of scope here. We only verify the engine actually + // invokes our behavior. + } + } + + public static class MyTestBoundaryBehavior extends BoundaryEventActivityBehavior { + + private static final long serialVersionUID = 1L; + // Records the keys observed during execute(). Test-only side channel so we can assert the engine + // actually invoked our behavior end-to-end. + public static final List RECORDED_KEYS = new CopyOnWriteArrayList<>(); + + protected final String customKey; + + public MyTestBoundaryBehavior(String customKey, boolean interrupting) { + super(interrupting); + this.customKey = customKey; + } + + public String getCustomKey() { + return customKey; + } + + @Override + public void execute(DelegateExecution execution) { + RECORDED_KEYS.add("execute-" + customKey); + super.execute(execution); + } + + @Override + public void trigger(DelegateExecution execution, String triggerName, Object triggerData) { + RECORDED_KEYS.add("trigger-" + customKey); + super.trigger(execution, triggerName, triggerData); + } + } + + public static class MyTestProcessStartBehavior extends FlowNodeActivityBehavior implements ProcessLevelStartEventActivityBehavior { + + private static final long serialVersionUID = 1L; + + public static final String OBSOLETE_TYPE = "myTestProcessStartObsoleteType"; + + // Suffix [restoring] marks deploys where ProcessLevelStartEventDeployContext.isRestoringPreviousVersion() is true. + public static final List RECORDED_KEYS = new CopyOnWriteArrayList<>(); + + protected final String customKey; + + public MyTestProcessStartBehavior(String customKey) { + this.customKey = customKey; + } + + public String getCustomKey() { + return customKey; + } + + @Override + public void deploy(ProcessLevelStartEventDeployContext context) { + String suffix = context.isRestoringPreviousVersion() ? "[restoring]" : ""; + RECORDED_KEYS.add("deploy-" + customKey + "@" + context.getProcessDefinition().getVersion() + suffix); + context.getEventSubscriptionService().createEventSubscriptionBuilder() + .eventType(OBSOLETE_TYPE) + .activityId(context.getStartEvent().getId()) + .processDefinitionId(context.getProcessDefinition().getId()) + .scopeType(ScopeTypes.BPMN) + .create(); + } + + @Override + public void undeploy(ProcessLevelStartEventUndeployContext context) { + RECORDED_KEYS.add("undeploy-" + customKey + "@" + context.getPreviousProcessDefinition().getVersion()); + // Exercises the bulk-flush hook: the deployer iterates registered types and bulk-deletes + // matching event subscriptions on the previous process definition. + context.registerObsoleteEventSubscriptionType(OBSOLETE_TYPE); + } + } +} diff --git a/modules/flowable-engine/src/test/java/org/flowable/standalone/validation/DefaultProcessValidatorTest.java b/modules/flowable-engine/src/test/java/org/flowable/standalone/validation/DefaultProcessValidatorTest.java index f9dc37473d2..e5a5360436b 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/standalone/validation/DefaultProcessValidatorTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/standalone/validation/DefaultProcessValidatorTest.java @@ -27,7 +27,15 @@ import javax.xml.stream.XMLStreamReader; import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BoundaryEvent; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.EndEvent; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.SequenceFlow; +import org.flowable.bpmn.model.StartEvent; +import org.flowable.bpmn.model.TerminateEventDefinition; +import org.flowable.bpmn.model.TimerEventDefinition; +import org.flowable.bpmn.model.UserTask; import org.flowable.common.engine.api.io.InputStreamProvider; import org.flowable.engine.test.util.TestProcessUtil; import org.flowable.validation.ProcessValidationContextImpl; @@ -360,7 +368,7 @@ void testEventSubProcessWithCancelStart() { assertThat(errors).hasSize(1); ValidationError error = errors.get(0); assertThat(error.getProblem()).isEqualTo(Problems.EVENT_SUBPROCESS_INVALID_START_EVENT_DEFINITION); - assertThat(error.getDefaultDescription()).isEqualTo("start event of event subprocess must be of type 'error', 'timer', 'message' or 'signal'"); + assertThat(error.getDefaultDescription()).isEqualTo("Unsupported event subprocess start event definition type"); assertThat(error.getActivityId()).isEqualTo("cancelStartEventSubProcess"); assertThat(error.getActivityName()).isEqualTo("Cancel Event Sub Process"); assertThat(error.isWarning()).isFalse(); @@ -368,6 +376,76 @@ void testEventSubProcessWithCancelStart() { assertThat(error.getXmlColumnNumber()).isEqualTo(41); } + @Test + void testBoundaryEventWithUnsupportedDefinition() { + BpmnModel bpmnModel = new BpmnModel(); + Process process = new Process(); + process.setId("boundaryEventWithUnsupportedDefinition"); + process.setExecutable(true); + bpmnModel.addProcess(process); + + StartEvent start = new StartEvent(); + start.setId("start"); + process.addFlowElement(start); + + UserTask userTask = new UserTask(); + userTask.setId("theTask"); + userTask.setName("The Task"); + process.addFlowElement(userTask); + + BoundaryEvent boundaryEvent = new BoundaryEvent(); + boundaryEvent.setId("invalidBoundaryEvent"); + boundaryEvent.setName("Invalid Boundary Event"); + boundaryEvent.setAttachedToRef(userTask); + boundaryEvent.setAttachedToRefId(userTask.getId()); + boundaryEvent.addEventDefinition(new TerminateEventDefinition()); + process.addFlowElement(boundaryEvent); + + EndEvent end = new EndEvent(); + end.setId("end"); + process.addFlowElement(end); + + process.addFlowElement(new SequenceFlow("start", "theTask")); + process.addFlowElement(new SequenceFlow("theTask", "end")); + + List errors = processValidator.validate(bpmnModel); + + assertThat(errors) + .extracting(ValidationError::getProblem, ValidationError::getDefaultDescription, ValidationError::getActivityId, ValidationError::isWarning) + .containsExactly( + tuple(Problems.BOUNDARY_EVENT_INVALID_EVENT_DEFINITION, "Invalid or unsupported event definition", "invalidBoundaryEvent", false)); + } + + @Test + void testEndEventWithUnsupportedDefinition() { + BpmnModel bpmnModel = new BpmnModel(); + Process process = new Process(); + process.setId("endEventWithUnsupportedDefinition"); + process.setExecutable(true); + bpmnModel.addProcess(process); + + StartEvent start = new StartEvent(); + start.setId("start"); + process.addFlowElement(start); + + EndEvent end = new EndEvent(); + end.setId("invalidEndEvent"); + end.setName("Invalid End Event"); + TimerEventDefinition timerEventDefinition = new TimerEventDefinition(); + timerEventDefinition.setTimeDuration("PT5M"); + end.addEventDefinition(timerEventDefinition); + process.addFlowElement(end); + + process.addFlowElement(new SequenceFlow("start", "invalidEndEvent")); + + List errors = processValidator.validate(bpmnModel); + + assertThat(errors) + .extracting(ValidationError::getProblem, ValidationError::getDefaultDescription, ValidationError::getActivityId, ValidationError::isWarning) + .containsExactly( + tuple(Problems.END_EVENT_INVALID_EVENT_DEFINITION, "Invalid or unsupported event definition", "invalidEndEvent", false)); + } + @Test void testInvalidMultiInstanceActivity() { BpmnModel bpmnModel = readBpmnModelFromXml("org/flowable/standalone/validation/invalidMultiInstanceActivity.bpmn20.xml"); diff --git a/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.boundary.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.boundary.bpmn20.xml new file mode 100644 index 00000000000..4e861fc2c57 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.boundary.bpmn20.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.bpmn20.xml new file mode 100644 index 00000000000..df09c07ce87 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.bpmn20.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.eventSubProcess.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.eventSubProcess.bpmn20.xml new file mode 100644 index 00000000000..06999b212ab --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.eventSubProcess.bpmn20.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml new file mode 100644 index 00000000000..513abb6323c --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/standalone/parsing/CustomEventDefinitionTest.processStart.bpmn20.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/EventSubscriptionService.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/EventSubscriptionService.java index f1438ab67c9..1880a5cea5c 100644 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/EventSubscriptionService.java +++ b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/EventSubscriptionService.java @@ -12,6 +12,7 @@ */ package org.flowable.eventsubscription.service; +import java.util.Collection; import java.util.List; import org.flowable.eventsubscription.api.EventSubscription; @@ -41,7 +42,15 @@ public interface EventSubscriptionService { List findEventSubscriptionsByProcessInstanceAndActivityId(String processInstanceId, String activityId, String type); - List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId); + List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId); + + /** + * Returns case-definition-level event subscriptions (i.e. {@code SCOPE_ID_ IS NULL}) for the given + * scope definition + scope type whose event type is in the supplied collection. Empty types yields an + * empty list. Mirrors {@link #findEventSubscriptionsByTypesAndProcessDefinitionId} for the CMMN side. + */ + List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, + String scopeDefinitionId, String scopeType, String tenantId); List findEventSubscriptionsByExecutionAndType(String executionId, String type); @@ -93,8 +102,6 @@ public interface EventSubscriptionService { void deleteEventSubscriptionsForScopeDefinitionIdAndType(String scopeDefinitionId, String scopeType); - void deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(String scopeDefinitionId, String scopeType); - void deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(String processDefinitionId, String eventType, String activityId, String configuration); void deleteEventSubscriptionsForScopeDefinitionAndScopeStartEvent(String scopeDefinitionId, String eventType, String configuration); diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/EventSubscriptionServiceImpl.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/EventSubscriptionServiceImpl.java index 2a4ca08f2c1..2129e63460f 100644 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/EventSubscriptionServiceImpl.java +++ b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/EventSubscriptionServiceImpl.java @@ -12,6 +12,7 @@ */ package org.flowable.eventsubscription.service.impl; +import java.util.Collection; import java.util.List; import org.flowable.common.engine.impl.service.CommonServiceImpl; @@ -65,8 +66,14 @@ public List findEventSubscriptionsByProcessInstanceAndA } @Override - public List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId) { - return getEventSubscriptionEntityManager().findEventSubscriptionsByTypeAndProcessDefinitionId(type, processDefinitionId, tenantId); + public List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId) { + return getEventSubscriptionEntityManager().findEventSubscriptionsByTypesAndProcessDefinitionId(types, processDefinitionId, tenantId); + } + + @Override + public List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, String scopeDefinitionId, + String scopeType, String tenantId) { + return getEventSubscriptionEntityManager().findEventSubscriptionsByTypesAndScopeDefinitionId(eventTypes, scopeDefinitionId, scopeType, tenantId); } @Override @@ -194,11 +201,6 @@ public void deleteEventSubscriptionsForScopeDefinitionIdAndType(String scopeDefi getEventSubscriptionEntityManager().deleteEventSubscriptionsForScopeDefinitionIdAndType(scopeDefinitionId, scopeType); } - @Override - public void deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(String scopeDefinitionId, String scopeType) { - getEventSubscriptionEntityManager().deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(scopeDefinitionId, scopeType); - } - @Override public void deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(String processDefinitionId, String eventType, String activityId, String configuration) { getEventSubscriptionEntityManager().deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(processDefinitionId, eventType, activityId, configuration); diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManager.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManager.java index 977e9b30aec..01641fd7c1c 100644 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManager.java +++ b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManager.java @@ -12,6 +12,7 @@ */ package org.flowable.eventsubscription.service.impl.persistence.entity; +import java.util.Collection; import java.util.List; import org.flowable.common.engine.impl.persistence.entity.EntityManager; @@ -60,8 +61,6 @@ public interface EventSubscriptionEntityManager extends EntityManager findEventSubscriptionsByProcessInstanceAndActivityId(String processInstanceId, String activityId, String type); - List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId); + List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId); + + List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, String scopeDefinitionId, String scopeType, String tenantId); List findEventSubscriptionsByScopeIdAndType(String scopeId, String type); diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManagerImpl.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManagerImpl.java index 29fa8f0f1d0..66e406c0e3f 100644 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManagerImpl.java +++ b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/EventSubscriptionEntityManagerImpl.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; @@ -168,8 +169,14 @@ public List findEventSubscriptionsBySubScopeId(final St } @Override - public List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId) { - return dataManager.findEventSubscriptionsByTypeAndProcessDefinitionId(type, processDefinitionId, tenantId); + public List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId) { + return dataManager.findEventSubscriptionsByTypesAndProcessDefinitionId(types, processDefinitionId, tenantId); + } + + @Override + public List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, String scopeDefinitionId, + String scopeType, String tenantId) { + return dataManager.findEventSubscriptionsByTypesAndScopeDefinitionId(eventTypes, scopeDefinitionId, scopeType, tenantId); } @Override @@ -247,11 +254,6 @@ public void deleteEventSubscriptionsForScopeDefinitionIdAndType(String scopeDefi dataManager.deleteEventSubscriptionsForScopeDefinitionIdAndType(scopeDefinitionId, scopeType); } - @Override - public void deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(String scopeDefinitionId, String scopeType) { - dataManager.deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(scopeDefinitionId, scopeType); - } - @Override public void deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(String processDefinitionId, String eventType, String activityId, String configuration) { dataManager.deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(processDefinitionId, eventType, activityId, configuration); diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/EventSubscriptionDataManager.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/EventSubscriptionDataManager.java index cb30ddd1b45..8375789c000 100644 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/EventSubscriptionDataManager.java +++ b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/EventSubscriptionDataManager.java @@ -12,6 +12,7 @@ */ package org.flowable.eventsubscription.service.impl.persistence.entity.data; +import java.util.Collection; import java.util.Date; import java.util.List; @@ -61,7 +62,9 @@ public interface EventSubscriptionDataManager extends DataManager findEventSubscriptionsBySubScopeId(final String subScopeId); - List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId); + List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId); + + List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, String scopeDefinitionId, String scopeType, String tenantId); List findEventSubscriptionsByScopeIdAndType(final String scopeId, final String type); @@ -89,8 +92,6 @@ public interface EventSubscriptionDataManager extends DataManager eventSubscriptionsByScopeDefinitionIdAndTypeMatcher = new EventSubscriptionsByScopeDefinitionIdAndTypeMatcher(); - protected CachedEntityMatcher eventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher = new EventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher(); protected CachedEntityMatcher eventSubscriptionsByScopeIdAndTypeMatcher = new EventSubscriptionsByScopeIdAndTypeMatcher(); @@ -257,17 +257,34 @@ public List findEventSubscriptionsBySubScopeId(final St @Override @SuppressWarnings("unchecked") - public List findEventSubscriptionsByTypeAndProcessDefinitionId(String type, String processDefinitionId, String tenantId) { - final String query = "selectEventSubscriptionsByTypeAndProcessDefinitionId"; - Map params = new HashMap<>(); - if (type != null) { - params.put("eventType", type); + public List findEventSubscriptionsByTypesAndProcessDefinitionId(Collection types, String processDefinitionId, String tenantId) { + if (types == null || types.isEmpty()) { + return Collections.emptyList(); } + Map params = new HashMap<>(); + params.put("eventTypes", types); params.put("processDefinitionId", processDefinitionId); if (tenantId != null && !tenantId.equals(EventSubscriptionServiceConfiguration.NO_TENANT_ID)) { params.put("tenantId", tenantId); } - return getDbSqlSession().selectList(query, params); + return getDbSqlSession().selectList("selectEventSubscriptionsByTypesAndProcessDefinitionId", params); + } + + @Override + @SuppressWarnings("unchecked") + public List findEventSubscriptionsByTypesAndScopeDefinitionId(Collection eventTypes, String scopeDefinitionId, + String scopeType, String tenantId) { + if (eventTypes == null || eventTypes.isEmpty()) { + return Collections.emptyList(); + } + Map params = new HashMap<>(); + params.put("eventTypes", eventTypes); + params.put("scopeDefinitionId", scopeDefinitionId); + params.put("scopeType", scopeType); + if (tenantId != null && !tenantId.equals(EventSubscriptionServiceConfiguration.NO_TENANT_ID)) { + params.put("tenantId", tenantId); + } + return getDbSqlSession().selectList("selectEventSubscriptionsByTypesAndScopeDefinitionId", params); } @Override @@ -397,15 +414,6 @@ public void deleteEventSubscriptionsForScopeDefinitionIdAndType(String scopeDefi bulkDelete("deleteEventSubscriptionsForScopeDefinitionIdAndType", eventSubscriptionsByScopeDefinitionIdAndTypeMatcher, params); } - @Override - public void deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(String scopeDefinitionId, String scopeType) { - Map params = new HashMap<>(); - params.put("scopeDefinitionId", scopeDefinitionId); - params.put("scopeType", scopeType); - bulkDelete("deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId", - eventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher, params); - } - @Override public void deleteEventSubscriptionsForProcessDefinitionAndProcessStartEvent(String processDefinitionId, String eventType, String activityId, String configuration) { Map params = new HashMap<>(); diff --git a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/impl/cachematcher/EventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher.java b/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/impl/cachematcher/EventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher.java deleted file mode 100644 index 4fb7bfa63c2..00000000000 --- a/modules/flowable-eventsubscription-service/src/main/java/org/flowable/eventsubscription/service/impl/persistence/entity/data/impl/cachematcher/EventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher.java +++ /dev/null @@ -1,38 +0,0 @@ -/* Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.flowable.eventsubscription.service.impl.persistence.entity.data.impl.cachematcher; - -import java.util.Map; -import java.util.Objects; - -import org.flowable.common.engine.impl.persistence.cache.CachedEntityMatcherAdapter; -import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity; - -/** - * @author Joram Barrez - */ -public class EventSubscriptionsByScopeDefinitionIdAndTypeAndNullScopeIdMatcher extends CachedEntityMatcherAdapter { - - @Override - public boolean isRetained(EventSubscriptionEntity eventSubscriptionEntity, Object parameter) { - Map params = (Map) parameter; - - String scopeDefinitionId = params.get("scopeDefinitionId"); - String scopeType = params.get("scopeType"); - - return Objects.equals(scopeDefinitionId, eventSubscriptionEntity.getScopeDefinitionId()) - && Objects.equals(scopeType, eventSubscriptionEntity.getScopeType()) - && eventSubscriptionEntity.getScopeId() == null; - } - -} \ No newline at end of file diff --git a/modules/flowable-eventsubscription-service/src/main/resources/org/flowable/eventsubscription/service/db/mapping/entity/EventSubscription.xml b/modules/flowable-eventsubscription-service/src/main/resources/org/flowable/eventsubscription/service/db/mapping/entity/EventSubscription.xml index dec505c84ba..e717f42d2d0 100644 --- a/modules/flowable-eventsubscription-service/src/main/resources/org/flowable/eventsubscription/service/db/mapping/entity/EventSubscription.xml +++ b/modules/flowable-eventsubscription-service/src/main/resources/org/flowable/eventsubscription/service/db/mapping/entity/EventSubscription.xml @@ -322,13 +322,14 @@ where (SUB_SCOPE_ID_ = #{parameter, jdbcType=NVARCHAR}) - select * from ${prefix}ACT_RU_EVENT_SUBSCR - - (EVENT_TYPE_ = #{parameter.eventType, jdbcType=NVARCHAR}) - + EVENT_TYPE_ in + + #{eventType, jdbcType=NVARCHAR} + and PROC_DEF_ID_ = #{parameter.processDefinitionId, jdbcType=NVARCHAR} and EXECUTION_ID_ is null and PROC_INST_ID_ is null @@ -341,6 +342,26 @@ + +