diff --git a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/constants/BpmnXMLConstants.java b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/constants/BpmnXMLConstants.java index b178076375be17f062e491ce8ea3a9f7b9a414a3..5a8ce67123554252725084c7f96ca802b0da5d49 100644 --- a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/constants/BpmnXMLConstants.java +++ b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/constants/BpmnXMLConstants.java @@ -69,9 +69,19 @@ public interface BpmnXMLConstants { public static final String ELEMENT_TASK_LISTENER = "taskListener"; public static final String ATTRIBUTE_LISTENER_EVENT = "event"; public static final String ATTRIBUTE_LISTENER_EVENTS = "events"; + public static final String ATTRIBUTE_LISTENER_ENTITY_TYPE = "entityType"; public static final String ATTRIBUTE_LISTENER_CLASS = "class"; public static final String ATTRIBUTE_LISTENER_EXPRESSION = "expression"; public static final String ATTRIBUTE_LISTENER_DELEGATEEXPRESSION = "delegateExpression"; + public static final String ATTRIBUTE_LISTENER_THROW_EVENT_TYPE = "throwEvent"; + public static final String ATTRIBUTE_LISTENER_THROW_SIGNAL_EVENT_NAME = "signalName"; + public static final String ATTRIBUTE_LISTENER_THROW_MESSAGE_EVENT_NAME = "messageName"; + public static final String ATTRIBUTE_LISTENER_THROW_ERROR_EVENT_CODE = "errorCode"; + + public static final String ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_SIGNAL = "signal"; + public static final String ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_GLOBAL_SIGNAL = "globalSignal"; + public static final String ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_MESSAGE = "message"; + public static final String ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_ERROR = "error"; public static final String ATTRIBUTE_VALUE_TRUE = "true"; public static final String ATTRIBUTE_VALUE_FALSE = "false"; diff --git a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/child/ActivitiEventListenerParser.java b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/child/ActivitiEventListenerParser.java index 5dd292a2db90e77cea6579c4578dc61ce4dcad96..9c8fcb0b955190e168da0ec56a0d424c4ad1ae54 100644 --- a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/child/ActivitiEventListenerParser.java +++ b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/child/ActivitiEventListenerParser.java @@ -28,7 +28,6 @@ import org.apache.commons.lang3.StringUtils; public class ActivitiEventListenerParser extends BaseChildElementParser { public void parseChildElement(XMLStreamReader xtr, BaseElement parentElement, BpmnModel model) throws Exception { - EventListener listener = new EventListener(); BpmnXMLUtil.addXMLLocation(listener, xtr); if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_CLASS))) { @@ -37,10 +36,28 @@ public class ActivitiEventListenerParser extends BaseChildElementParser { } else if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_DELEGATEEXPRESSION))) { listener.setImplementation(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_DELEGATEEXPRESSION)); listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + } else if(StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_EVENT_TYPE))) { + String eventType = xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_EVENT_TYPE); + if(ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_SIGNAL.equals(eventType)) { + listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT); + listener.setImplementation(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_SIGNAL_EVENT_NAME)); + } else if(ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_GLOBAL_SIGNAL.equals(eventType)) { + listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_THROW_GLOBAL_SIGNAL_EVENT); + listener.setImplementation(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_SIGNAL_EVENT_NAME)); + } else if(ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_MESSAGE.equals(eventType)) { + listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT); + listener.setImplementation(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_MESSAGE_EVENT_NAME)); + } else if(ATTRIBUTE_LISTENER_THROW_EVENT_TYPE_ERROR.equals(eventType)) { + listener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT); + listener.setImplementation(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_THROW_ERROR_EVENT_CODE)); + } else { + model.addProblem("Unsupported value of 'throwEvent' attribute: " + eventType, xtr); + } } else { - model.addProblem("Element 'class' or 'delegateExpression' is mandatory on eventListener", xtr); + model.addProblem("Element 'class', 'delegateExpression' or 'throwEvent' is mandatory on eventListener", xtr); } listener.setEvents(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_EVENTS)); + listener.setEntityType(xtr.getAttributeValue(null, ATTRIBUTE_LISTENER_ENTITY_TYPE)); Process parentProcess = (Process) parentElement; parentProcess.getEventListeners().add(listener); diff --git a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/export/ActivitiListenerExport.java b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/export/ActivitiListenerExport.java index 299dcbdd7522fdedb0d64afe9d5ef156f35693fe..01a3ac278b041b57eb0bc8d453be7d4b8b8879ce 100644 --- a/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/export/ActivitiListenerExport.java +++ b/modules/activiti-bpmn-converter/src/main/java/org/activiti/bpmn/converter/export/ActivitiListenerExport.java @@ -58,11 +58,19 @@ public class ActivitiListenerExport implements BpmnXMLConstants { xtw.writeStartElement(ACTIVITI_EXTENSIONS_PREFIX, ELEMENT_EVENT_LISTENER, ACTIVITI_EXTENSIONS_NAMESPACE); BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_EVENTS, eventListener.getEvents(), xtw); + BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_ENTITY_TYPE, eventListener.getEntityType(), xtw); if (ImplementationType.IMPLEMENTATION_TYPE_CLASS.equals(eventListener.getImplementationType())) { BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_CLASS, eventListener.getImplementation(), xtw); } else if (ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION.equals(eventListener.getImplementationType())) { BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_DELEGATEEXPRESSION, eventListener.getImplementation(), xtw); + } else if(ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT.equals(eventListener.getImplementationType()) + || ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT.equals(eventListener.getImplementationType())) { + BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_THROW_SIGNAL_EVENT_NAME, eventListener.getImplementation(), xtw); + } else if(ImplementationType.IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT.equals(eventListener.getImplementationType())) { + BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_THROW_MESSAGE_EVENT_NAME, eventListener.getImplementation(), xtw); + } else if(ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT.equals(eventListener.getImplementationType())) { + BpmnXMLUtil.writeDefaultAttribute(ATTRIBUTE_LISTENER_THROW_ERROR_EVENT_CODE, eventListener.getImplementation(), xtw); } xtw.writeEndElement(); diff --git a/modules/activiti-bpmn-converter/src/main/resources/org/activiti/impl/bpmn/parser/activiti-bpmn-extensions-5.15.xsd b/modules/activiti-bpmn-converter/src/main/resources/org/activiti/impl/bpmn/parser/activiti-bpmn-extensions-5.15.xsd index 5f0c57b68ea41be2fcfa68867613e8144be02f07..593d98425b8aa27749eefe26c9f82abe2be8bb2f 100644 --- a/modules/activiti-bpmn-converter/src/main/resources/org/activiti/impl/bpmn/parser/activiti-bpmn-extensions-5.15.xsd +++ b/modules/activiti-bpmn-converter/src/main/resources/org/activiti/impl/bpmn/parser/activiti-bpmn-extensions-5.15.xsd @@ -477,6 +477,78 @@ + + + + Extension element for defining event-listeners on a process-definition. + + + + + + + Comma-separated list of event-types an event-listener is configured to be notified of. Can also be a single event-type. + + + + + + + Type of entity that should be targeted by events for which the event-listener should be notified. + + + + + + + + + + + + + + + + + + + Type of event to be throw as a response to a matching activiti-event being dispatched. + + + + + + + + + + + + + + + Name of a signal. + + + + + + + Name of a message. + + + + + + + Error-code of an error event. + + + + + + diff --git a/modules/activiti-bpmn-converter/src/test/java/org/activiti/editor/language/xml/EventListenerConverterTest.java b/modules/activiti-bpmn-converter/src/test/java/org/activiti/editor/language/xml/EventListenerConverterTest.java index da5370ce524c3ae90632e645d715d8eab759275d..f52207663892ff2479f897c02e96bb9fc4d6eb98 100644 --- a/modules/activiti-bpmn-converter/src/test/java/org/activiti/editor/language/xml/EventListenerConverterTest.java +++ b/modules/activiti-bpmn-converter/src/test/java/org/activiti/editor/language/xml/EventListenerConverterTest.java @@ -4,9 +4,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import org.activiti.bpmn.constants.BpmnXMLConstants; import org.activiti.bpmn.model.BpmnModel; import org.activiti.bpmn.model.EventListener; +import org.activiti.bpmn.model.ImplementationType; import org.activiti.bpmn.model.Process; import org.junit.Test; @@ -30,24 +30,55 @@ public class EventListenerConverterTest extends AbstractConverterTest { Process process = model.getMainProcess(); assertNotNull(process); assertNotNull(process.getEventListeners()); - assertEquals(3, process.getEventListeners().size()); + assertEquals(8, process.getEventListeners().size()); // Listener with class EventListener listener = process.getEventListeners().get(0); assertEquals("ENTITY_CREATE", listener.getEvents()); assertEquals("org.activiti.test.MyListener", listener.getImplementation()); - assertEquals(BpmnXMLConstants.ATTRIBUTE_LISTENER_CLASS, listener.getImplementationType()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_CLASS, listener.getImplementationType()); // Listener with class, but no specific event (== all events) listener = process.getEventListeners().get(1); assertNull(listener.getEvents()); assertEquals("org.activiti.test.AllEventTypesListener", listener.getImplementation()); - assertEquals(BpmnXMLConstants.ATTRIBUTE_LISTENER_CLASS, listener.getImplementationType()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_CLASS, listener.getImplementationType()); // Listener with delegate expression listener = process.getEventListeners().get(2); assertEquals("ENTITY_DELETE", listener.getEvents()); assertEquals("${myListener}", listener.getImplementation()); - assertEquals(BpmnXMLConstants.ATTRIBUTE_LISTENER_DELEGATEEXPRESSION, listener.getImplementationType()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION, listener.getImplementationType()); + + // Listener that throws a signal-event + listener = process.getEventListeners().get(3); + assertEquals("ENTITY_DELETE", listener.getEvents()); + assertEquals("theSignal", listener.getImplementation()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT, listener.getImplementationType()); + + // Listener that throws a global signal-event + listener = process.getEventListeners().get(4); + assertEquals("ENTITY_DELETE", listener.getEvents()); + assertEquals("theSignal", listener.getImplementation()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_THROW_GLOBAL_SIGNAL_EVENT, listener.getImplementationType()); + + // Listener that throws a message-event + listener = process.getEventListeners().get(5); + assertEquals("ENTITY_DELETE", listener.getEvents()); + assertEquals("theMessage", listener.getImplementation()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT, listener.getImplementationType()); + + // Listener that throws an error-event + listener = process.getEventListeners().get(6); + assertEquals("ENTITY_DELETE", listener.getEvents()); + assertEquals("123", listener.getImplementation()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT, listener.getImplementationType()); + + // Listener restricted to a specific entity + listener = process.getEventListeners().get(7); + assertEquals("ENTITY_DELETE", listener.getEvents()); + assertEquals("123", listener.getImplementation()); + assertEquals(ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT, listener.getImplementationType()); + assertEquals("job", listener.getEntityType()); } } diff --git a/modules/activiti-bpmn-converter/src/test/resources/eventlistenersmodel.bpmn20.xml b/modules/activiti-bpmn-converter/src/test/resources/eventlistenersmodel.bpmn20.xml index 7d7671cd481e3aad6c35f459fec30728f8744b20..050c831c333a8ae15ac003a0c042b5935314bc01 100644 --- a/modules/activiti-bpmn-converter/src/test/resources/eventlistenersmodel.bpmn20.xml +++ b/modules/activiti-bpmn-converter/src/test/resources/eventlistenersmodel.bpmn20.xml @@ -5,6 +5,11 @@ + + + + + diff --git a/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/EventListener.java b/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/EventListener.java index a9ba4e363f4f6bb653a73afec5d8f8879e54a09f..655855b0f9420afd08f7f7ae77fd51052699804c 100644 --- a/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/EventListener.java +++ b/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/EventListener.java @@ -23,6 +23,7 @@ public class EventListener extends BaseElement { protected String events; protected String implementationType; protected String implementation; + protected String entityType; public String getEvents() { return events; @@ -42,6 +43,12 @@ public class EventListener extends BaseElement { public void setImplementation(String implementation) { this.implementation = implementation; } + public void setEntityType(String entityType) { + this.entityType = entityType; + } + public String getEntityType() { + return entityType; + } public EventListener clone() { EventListener clone = new EventListener(); @@ -53,5 +60,6 @@ public class EventListener extends BaseElement { setEvents(otherListener.getEvents()); setImplementation(otherListener.getImplementation()); setImplementationType(otherListener.getImplementationType()); + setEntityType(otherListener.getEntityType()); } } diff --git a/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/ImplementationType.java b/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/ImplementationType.java index d7d84788f2203b6ac4367569387df27d87e39299..1c8f20fea49d7a87c067ff810a742248d52f3fb3 100644 --- a/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/ImplementationType.java +++ b/modules/activiti-bpmn-model/src/main/java/org/activiti/bpmn/model/ImplementationType.java @@ -17,6 +17,10 @@ public class ImplementationType { public static String IMPLEMENTATION_TYPE_CLASS = "class"; public static String IMPLEMENTATION_TYPE_EXPRESSION = "expression"; public static String IMPLEMENTATION_TYPE_DELEGATEEXPRESSION = "delegateExpression"; + public static String IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT = "throwSignalEvent"; + public static String IMPLEMENTATION_TYPE_THROW_GLOBAL_SIGNAL_EVENT = "throwGlobalSignalEvent"; + public static String IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT = "throwMessageEvent"; + public static String IMPLEMENTATION_TYPE_THROW_ERROR_EVENT = "throwErrorEvent"; public static String IMPLEMENTATION_TYPE_WEBSERVICE = "##WebService"; } diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiErrorEvent.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiErrorEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..351cb9e9852f597e15d52e29a6c99af7136f8569 --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiErrorEvent.java @@ -0,0 +1,28 @@ +/* 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.activiti.engine.delegate.event; + + +/** + * An {@link ActivitiEvent} related to an error being sent to an activity. + * + * @author Frederik Heremans + */ +public interface ActivitiErrorEvent extends ActivitiActivityEvent { + + /** + * @return the error-code of the error. Returns null, if no specific error-code has been specified + * when the error was thrown. + */ + public String getErrorCode(); +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventListener.java index 571cdb01676894ec6d97f6d08f299d318f417926..b08c957887fcf86daa3b10d401b9bc94351013c1 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventListener.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventListener.java @@ -13,7 +13,7 @@ package org.activiti.engine.delegate.event; /** - * Describes a class that listens for {@link ActivitiEvent} dispatched by the engine. + * Describes a class that listens for {@link ActivitiEvent}s dispatched by the engine. * * @author Frederik Heremans */ diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventType.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventType.java index 9a129f104dfa1a8428d4a09ea9e24640da0aa4b0..d184affa68ad31a0fa061dd91de6ae3e77a96f8c 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventType.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/ActivitiEventType.java @@ -88,7 +88,7 @@ public enum ActivitiEventType { ENGINE_CLOSED, /** - * An activiyt is starting to execute. This event is dispatch right before an activity is executed. + * An activity is starting to execute. This event is dispatch right before an activity is executed. */ ACTIVITY_STARTED, @@ -115,6 +115,18 @@ public enum ActivitiEventType { */ ACTIVITY_MESSAGE_RECEIVED, + /** + * An activity has received an error event. Dispatched before the actual error has been received by + * the activity. This event will be either followed by a {@link #ACTIVITY_SIGNALLED} event or {@link #ACTIVITY_COMPLETE} + * for the involved activity, if the error was delivered successfully. + */ + ACTIVITY_ERROR_RECEIVED, + + /** + * When a BPMN Error was thrown, but was not caught within in the process. + */ + UNCAUGHT_BPMN_ERROR, + /** * A new variable has been created. */ diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiErrorEventImpl.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiErrorEventImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5397478f379ef58f2db578cc6ecfffe571655965 --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiErrorEventImpl.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.activiti.engine.delegate.event.impl; + +import org.activiti.engine.delegate.event.ActivitiErrorEvent; +import org.activiti.engine.delegate.event.ActivitiEventType; + +/** + * Implementation of an {@link ActivitiErrorEvent}. + * @author Frederik Heremans + */ +public class ActivitiErrorEventImpl extends ActivitiActivityEventImpl implements ActivitiErrorEvent { + + protected String errorCode; + + public ActivitiErrorEventImpl(ActivitiEventType type) { + super(type); + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + @Override + public String getErrorCode() { + return errorCode; + } + +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventBuilder.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventBuilder.java index 9116f045d5b25bc4f1914ed5ee1e184bdbc402c5..1d63fe13c288b526fc0e9e8c232553823463843c 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventBuilder.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventBuilder.java @@ -15,6 +15,7 @@ package org.activiti.engine.delegate.event.impl; import org.activiti.engine.delegate.DelegateExecution; import org.activiti.engine.delegate.event.ActivitiActivityEvent; import org.activiti.engine.delegate.event.ActivitiEntityEvent; +import org.activiti.engine.delegate.event.ActivitiErrorEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventType; import org.activiti.engine.delegate.event.ActivitiExceptionEvent; @@ -25,8 +26,8 @@ import org.activiti.engine.delegate.event.ActivitiVariableEvent; import org.activiti.engine.impl.context.Context; import org.activiti.engine.impl.context.ExecutionContext; import org.activiti.engine.impl.persistence.entity.IdentityLinkEntity; +import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.runtime.Job; -import org.activiti.engine.task.Attachment; import org.activiti.engine.task.Task; /** @@ -46,6 +47,14 @@ public class ActivitiEventBuilder { return newEvent; } + public static ActivitiEvent createEvent(ActivitiEventType type, String executionId, String processInstanceId, String processDefinitionId) { + ActivitiEventImpl newEvent = new ActivitiEventImpl(type); + newEvent.setExecutionId(executionId); + newEvent.setProcessDefinitionId(processDefinitionId); + newEvent.setProcessInstanceId(processInstanceId); + return newEvent; + } + /** * @param type type of event * @param entity the entity this event targets @@ -139,6 +148,16 @@ public class ActivitiEventBuilder { return newEvent; } + public static ActivitiErrorEvent createErrorEvent(ActivitiEventType type, String activityId, String errorCode, String executionId, String processInstanceId, String processDefinitionId) { + ActivitiErrorEventImpl newEvent = new ActivitiErrorEventImpl(type); + newEvent.setActivityId(activityId); + newEvent.setExecutionId(executionId); + newEvent.setProcessDefinitionId(processDefinitionId); + newEvent.setProcessInstanceId(processInstanceId); + newEvent.setErrorCode(errorCode); + return newEvent; + } + public static ActivitiVariableEvent createVariableEvent(ActivitiEventType type, String variableName, Object variableValue, String taskId, String executionId, String processInstanceId, String processDefinitionId) { ActivitiVariableEventImpl newEvent = new ActivitiVariableEventImpl(type); @@ -199,6 +218,8 @@ public class ActivitiEventBuilder { event.setProcessInstanceId(((Task)persistendObject).getProcessInstanceId()); event.setExecutionId(((Task)persistendObject).getExecutionId()); event.setProcessDefinitionId(((Task)persistendObject).getProcessDefinitionId()); + } else if(persistendObject instanceof ProcessDefinition) { + event.setProcessDefinitionId(((ProcessDefinition) persistendObject).getId()); } } } diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventDispatcherImpl.java b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventDispatcherImpl.java index e2da85fefe8948c9b8f23567b987f4a30c32b928..c6b821945264d4494548dd9775a20ca3e8dfc51a 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventDispatcherImpl.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/delegate/event/impl/ActivitiEventDispatcherImpl.java @@ -12,12 +12,15 @@ */ package org.activiti.engine.delegate.event.impl; +import org.activiti.engine.delegate.event.ActivitiEntityEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventDispatcher; import org.activiti.engine.delegate.event.ActivitiEventListener; import org.activiti.engine.delegate.event.ActivitiEventType; import org.activiti.engine.impl.context.Context; +import org.activiti.engine.impl.interceptor.CommandContext; import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity; +import org.activiti.engine.repository.ProcessDefinition; /** * Class capable of dispatching events. @@ -28,55 +31,95 @@ public class ActivitiEventDispatcherImpl implements ActivitiEventDispatcher { protected ActivitiEventSupport eventSupport; protected boolean enabled = true; - + public ActivitiEventDispatcherImpl() { eventSupport = new ActivitiEventSupport(); - } - + } + public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - + this.enabled = enabled; + } + public boolean isEnabled() { - return enabled; - } + return enabled; + } @Override - public void addEventListener(ActivitiEventListener listenerToAdd) { - eventSupport.addEventListener(listenerToAdd); - } + public void addEventListener(ActivitiEventListener listenerToAdd) { + eventSupport.addEventListener(listenerToAdd); + } @Override - public void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... types) { + public void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... types) { eventSupport.addEventListener(listenerToAdd, types); - } + } @Override - public void removeEventListener(ActivitiEventListener listenerToRemove) { + public void removeEventListener(ActivitiEventListener listenerToRemove) { eventSupport.removeEventListener(listenerToRemove); - } + } @Override - public void dispatchEvent(ActivitiEvent event) { - if(enabled) { + public void dispatchEvent(ActivitiEvent event) { + if (enabled) { eventSupport.dispatchEvent(event); } - - // Check if a process context is active. If so, we also call the process-definition + + // Check if a process context is active. If so, we also call the + // process-definition // specific listeners (if any). - if(Context.isExecutionContextActive()) { + if (Context.isExecutionContextActive()) { ProcessDefinitionEntity definition = Context.getExecutionContext().getProcessDefinition(); - if(definition != null) { + if (definition != null) { definition.getEventSupport().dispatchEvent(event); } } else { -// // Try getting hold of the Process definition, based on the process definition-key, if a -// // context is active -// CommandContext commandContext = Context.getCommandContext(); -// if(commandContext != null) { -// commandContext.getProcessEngineConfiguration().getDeploymentManager() -// .resolveProcessDefinition(processDefinition) -// } + // Try getting hold of the Process definition, based on the process + // definition-key, if a + // context is active + CommandContext commandContext = Context.getCommandContext(); + if (commandContext != null) { + ProcessDefinitionEntity processDefinition = extractProcessDefinitionEntityFromEvent(event); + if (processDefinition != null) { + processDefinition.getEventSupport().dispatchEvent(event); + } + } } - } + } + + /** + * In case no process-context is active, this method attempts to extract a + * process-definition based on the event. In case it's an event related to an + * entity, this can be deducted by inspecting the entity, without additional + * queries to the database. + * + * If not an entity-related event, the process-definition will be retrieved + * based on the processDefinitionId (if filled in). This requires an + * additional query to the database in case not already cached. However, + * queries will only occur when the definition is not yet in the cache, which + * is very unlikely to happen, unless evicted. + * + * @param event + * @return + */ + protected ProcessDefinitionEntity extractProcessDefinitionEntityFromEvent(ActivitiEvent event) { + ProcessDefinitionEntity result = null; + + if (event.getProcessDefinitionId() != null) { + result = Context.getProcessEngineConfiguration().getDeploymentManager().getProcessDefinitionCache() + .get(event.getProcessDefinitionId()); + if (result != null) { + result = Context.getProcessEngineConfiguration().getDeploymentManager().resolveProcessDefinition(result); + } + } + + if(result == null && event instanceof ActivitiEntityEvent) { + Object entity = ((ActivitiEntityEvent) event).getEntity(); + if(entity instanceof ProcessDefinition) { + result = (ProcessDefinitionEntity) entity; + } + } + return result; + } + } diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/BaseDelegateEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/BaseDelegateEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..384740243b8174dd56ef6429b8ccb43a3eb512b7 --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/BaseDelegateEventListener.java @@ -0,0 +1,49 @@ +/* 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.activiti.engine.impl.bpmn.helper; + +import org.activiti.engine.delegate.event.ActivitiEntityEvent; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; + +/** + * Base implementation of a {@link ActivitiEventListener}, used when creating event-listeners + * that are part of a BPMN definition. + * + * @author Frederik Heremans + */ +public abstract class BaseDelegateEventListener implements ActivitiEventListener { + + protected Class entityClass; + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } + + protected boolean isValidEvent(ActivitiEvent event) { + boolean valid = false; + if(entityClass != null) { + if(event instanceof ActivitiEntityEvent) { + Object entity = ((ActivitiEntityEvent) event).getEntity(); + if(entity != null) { + valid = entityClass.isAssignableFrom(entity.getClass()); + } + } + } else { + // If no class is specified, all events are valid + valid = true; + } + return valid; + } + +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateActivitiEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateActivitiEventListener.java index 26a4a6c7f52707e9dc6c453f1fe60e02691d97af..b34808e7b1cb770597316ee3a59093ec4ff0610f 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateActivitiEventListener.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateActivitiEventListener.java @@ -13,6 +13,7 @@ package org.activiti.engine.impl.bpmn.helper; import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.delegate.event.ActivitiEntityEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventListener; import org.activiti.engine.impl.util.ReflectUtil; @@ -20,22 +21,29 @@ import org.activiti.engine.impl.util.ReflectUtil; /** * An {@link ActivitiEventListener} implementation which uses a classname to * create a delegate {@link ActivitiEventListener} instance to use for event notification. + *

+ * + * In case an entityClass was passed in the constructor, only events that are {@link ActivitiEntityEvent}'s + * that target an entity of the given type, are dispatched to the delegate. * * @author Frederik Heremans */ -public class DelegateActivitiEventListener implements ActivitiEventListener { +public class DelegateActivitiEventListener extends BaseDelegateEventListener { protected String className; protected ActivitiEventListener delegateInstance; protected boolean failOnException = true; - public DelegateActivitiEventListener(String className) { + public DelegateActivitiEventListener(String className, Class entityClass) { this.className = className; + setEntityClass(entityClass); } @Override public void onEvent(ActivitiEvent event) { - getDelegateInstance().onEvent(event); + if(isValidEvent(event)) { + getDelegateInstance().onEvent(event); + } } @Override diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateExpressionActivitiEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateExpressionActivitiEventListener.java index e70ca8c9c17f01464615d1cab72956ea34a13c28..1fdbdc11c00f0729bc2dfa363bd3cc7c1e296405 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateExpressionActivitiEventListener.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/DelegateExpressionActivitiEventListener.java @@ -14,6 +14,7 @@ package org.activiti.engine.impl.bpmn.helper; import org.activiti.engine.ActivitiIllegalArgumentException; import org.activiti.engine.delegate.Expression; +import org.activiti.engine.delegate.event.ActivitiEntityEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventListener; import org.activiti.engine.impl.el.NoExecutionVariableScope; @@ -21,39 +22,44 @@ import org.activiti.engine.impl.el.NoExecutionVariableScope; /** * An {@link ActivitiEventListener} implementation which resolves an expression * to a delegate {@link ActivitiEventListener} instance and uses this for event notification. - * + *

+ * In case an entityClass was passed in the constructor, only events that are {@link ActivitiEntityEvent}'s + * that target an entity of the given type, are dispatched to the delegate. + * * @author Frederik Heremans */ -public class DelegateExpressionActivitiEventListener implements ActivitiEventListener { +public class DelegateExpressionActivitiEventListener extends BaseDelegateEventListener { protected Expression expression; - protected boolean failOnException = true; - public DelegateExpressionActivitiEventListener(Expression expression) { + public DelegateExpressionActivitiEventListener(Expression expression, Class entityClass) { this.expression = expression; + setEntityClass(entityClass); } @Override public void onEvent(ActivitiEvent event) { - NoExecutionVariableScope scope = new NoExecutionVariableScope(); - - Object delegate = expression.getValue(scope); - if (delegate instanceof ActivitiEventListener) { - // Cache result of isFailOnException() from delegate-instance untill next - // event is received. This prevents us from having to resolve the expression twice when - // an error occurs. - failOnException = ((ActivitiEventListener) delegate).isFailOnException(); - - // Call the delegate - ((ActivitiEventListener) delegate).onEvent(event); - } else { + if(isValidEvent(event)) { + NoExecutionVariableScope scope = new NoExecutionVariableScope(); - // Force failing, since the exception we're about to throw cannot be ignored, because it - // did not originate from the listener itself - failOnException = true; - throw new ActivitiIllegalArgumentException("Delegate expression " + expression - + " did not resolve to an implementation of " + ActivitiEventListener.class.getName()); + Object delegate = expression.getValue(scope); + if (delegate instanceof ActivitiEventListener) { + // Cache result of isFailOnException() from delegate-instance until next + // event is received. This prevents us from having to resolve the expression twice when + // an error occurs. + failOnException = ((ActivitiEventListener) delegate).isFailOnException(); + + // Call the delegate + ((ActivitiEventListener) delegate).onEvent(event); + } else { + + // Force failing, since the exception we're about to throw cannot be ignored, because it + // did not originate from the listener itself + failOnException = true; + throw new ActivitiIllegalArgumentException("Delegate expression " + expression + + " did not resolve to an implementation of " + ActivitiEventListener.class.getName()); + } } } diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorPropagation.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorPropagation.java index 36e27f1ddc463ef7641e88c8f96246c3b3a5e3f3..1ddb9d3f751f4339ee149970d28cfca538497896 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorPropagation.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorPropagation.java @@ -17,9 +17,12 @@ import java.util.List; import org.activiti.engine.ActivitiException; import org.activiti.engine.delegate.BpmnError; +import org.activiti.engine.delegate.event.ActivitiEventType; +import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder; import org.activiti.engine.impl.bpmn.behavior.EventSubProcessStartEventActivityBehavior; import org.activiti.engine.impl.bpmn.parser.BpmnParse; import org.activiti.engine.impl.bpmn.parser.ErrorEventDefinition; +import org.activiti.engine.impl.context.Context; import org.activiti.engine.impl.persistence.entity.ExecutionEntity; import org.activiti.engine.impl.pvm.PvmActivity; import org.activiti.engine.impl.pvm.PvmProcessDefinition; @@ -58,7 +61,7 @@ public class ErrorPropagation { // TODO: merge two approaches (super process / regular process approach) if(eventHandlerId != null) { - executeCatch(eventHandlerId, execution); + executeCatch(eventHandlerId, execution, errorCode); }else { ActivityExecution superExecution = getSuperExecution(execution); if (superExecution != null) { @@ -66,6 +69,11 @@ public class ErrorPropagation { } else { LOG.info("{} throws error event with errorCode '{}', but no catching boundary event was defined. Execution will simply be ended (none end event semantics).", execution.getActivity().getId(), errorCode); + + if(Context.getProcessEngineConfiguration() != null && Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) { + Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent( + ActivitiEventBuilder.createErrorEvent(ActivitiEventType.UNCAUGHT_BPMN_ERROR, null, errorCode, execution.getId(), execution.getProcessInstanceId(), execution.getProcessDefinitionId())); + } execution.end(); } } @@ -99,7 +107,7 @@ public class ErrorPropagation { private static void executeCatchInSuperProcess(String errorCode, ActivityExecution superExecution) { String errorHandlerId = findLocalErrorEventHandler(superExecution, errorCode); if (errorHandlerId != null) { - executeCatch(errorHandlerId, superExecution); + executeCatch(errorHandlerId, superExecution, errorCode); } else { // no matching catch found, going one level up in process hierarchy ActivityExecution superSuperExecution = getSuperExecution(superExecution); if (superSuperExecution != null) { @@ -120,7 +128,7 @@ public class ErrorPropagation { return superExecution; } - private static void executeCatch(String errorHandlerId, ActivityExecution execution) { + private static void executeCatch(String errorHandlerId, ActivityExecution execution, String errorCode) { ProcessDefinitionImpl processDefinition = ((ExecutionEntity) execution).getProcessDefinition(); ActivityImpl errorHandler = processDefinition.findActivity(errorHandlerId); if (errorHandler == null) { @@ -140,7 +148,7 @@ public class ErrorPropagation { } if(catchingScope instanceof PvmProcessDefinition) { - executeEventHandler(errorHandler, ((ExecutionEntity)execution).getProcessInstance()); + executeEventHandler(errorHandler, ((ExecutionEntity)execution).getProcessInstance(), errorCode); } else { if (currentActivity.getId().equals(catchingScope.getId())) { @@ -171,7 +179,7 @@ public class ErrorPropagation { } if (matchingParentFound && leavingExecution != null) { - executeEventHandler(errorHandler, leavingExecution); + executeEventHandler(errorHandler, leavingExecution, errorCode); } else { throw new ActivitiException("No matching parent execution for activity " + errorHandlerId + " found"); } @@ -179,7 +187,12 @@ public class ErrorPropagation { } - private static void executeEventHandler(ActivityImpl borderEventActivity, ActivityExecution leavingExecution) { + private static void executeEventHandler(ActivityImpl borderEventActivity, ActivityExecution leavingExecution, String errorCode) { + if(Context.getProcessEngineConfiguration() != null && Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) { + Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent( + ActivitiEventBuilder.createErrorEvent(ActivitiEventType.ACTIVITY_ERROR_RECEIVED, borderEventActivity.getId(), errorCode, leavingExecution.getId(), leavingExecution.getProcessInstanceId(), leavingExecution.getProcessDefinitionId())); + } + if(borderEventActivity.getActivityBehavior() instanceof EventSubProcessStartEventActivityBehavior) { InterpretableExecution execution = (InterpretableExecution) leavingExecution; execution.setActivity(borderEventActivity.getParentActivity()); diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorThrowingEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorThrowingEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e5143792e010fd049cd2def4d2e076b214a0731f --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/ErrorThrowingEventListener.java @@ -0,0 +1,65 @@ +/* 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.activiti.engine.impl.bpmn.helper; + +import org.activiti.engine.ActivitiException; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.impl.context.Context; +import org.activiti.engine.impl.persistence.entity.ExecutionEntity; + +/** + * An {@link ActivitiEventListener} that throws a error event when an event is + * dispatched to it. + * + * @author Frederik Heremans + * + */ +public class ErrorThrowingEventListener extends BaseDelegateEventListener { + + protected String errorCode; + + @Override + public void onEvent(ActivitiEvent event) { + if(isValidEvent(event)) { + ExecutionEntity execution = null; + + if (Context.isExecutionContextActive()) { + execution = Context.getExecutionContext().getExecution(); + } else if(event.getExecutionId() != null){ + // Get the execution based on the event's execution ID instead + execution = Context.getCommandContext().getExecutionEntityManager() + .findExecutionById(event.getExecutionId()); + } + + if(execution == null) { + throw new ActivitiException("No execution context active and event is not related to an execution. No compensation event can be thrown."); + } + + try { + ErrorPropagation.propagateError(errorCode, execution); + } catch (Exception e) { + throw new ActivitiException("Error while propagating error-event", e); + } + } + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + @Override + public boolean isFailOnException() { + return true; + } +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/MessageThrowingEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/MessageThrowingEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..a2210e75f9815a22af36e0cf4e3e8cc7b25c2612 --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/MessageThrowingEventListener.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.activiti.engine.impl.bpmn.helper; + +import java.util.List; + +import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.impl.context.Context; +import org.activiti.engine.impl.event.MessageEventHandler; +import org.activiti.engine.impl.interceptor.CommandContext; +import org.activiti.engine.impl.persistence.entity.EventSubscriptionEntity; + +/** + * An {@link ActivitiEventListener} that throws a message event when an event is + * dispatched to it. Sends the message to the execution the event was fired from. If the execution + * is not subscribed to a message, the process-instance is checked. + * + * @author Frederik Heremans + * + */ +public class MessageThrowingEventListener extends BaseDelegateEventListener { + + protected String messageName; + protected Class entityClass; + + @Override + public void onEvent(ActivitiEvent event) { + if(isValidEvent(event)) { + + if (event.getProcessInstanceId() == null) { + throw new ActivitiIllegalArgumentException( + "Cannot throw process-instance scoped message, since the dispatched event is not part of an ongoing process instance"); + } + + CommandContext commandContext = Context.getCommandContext(); + List subscriptionEntities = commandContext.getEventSubscriptionEntityManager() + .findEventSubscriptionsByNameAndExecution(MessageEventHandler.EVENT_HANDLER_TYPE, messageName, event.getExecutionId()); + + // Revert to messaging the process instance + if(subscriptionEntities.isEmpty() && event.getProcessInstanceId() != null && !event.getExecutionId().equals(event.getProcessInstanceId())) { + subscriptionEntities = commandContext.getEventSubscriptionEntityManager() + .findEventSubscriptionsByNameAndExecution(MessageEventHandler.EVENT_HANDLER_TYPE, messageName, event.getProcessInstanceId()); + } + + for (EventSubscriptionEntity signalEventSubscriptionEntity : subscriptionEntities) { + signalEventSubscriptionEntity.eventReceived(null, false); + } + } + } + + public void setMessageName(String messageName) { + this.messageName = messageName; + } + + @Override + public boolean isFailOnException() { + return true; + } +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/SignalThrowingEventListener.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/SignalThrowingEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..e6598d542e1a0bd1c8bad65a3b201b57196e2683 --- /dev/null +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/helper/SignalThrowingEventListener.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.activiti.engine.impl.bpmn.helper; + +import java.util.List; + +import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.impl.context.Context; +import org.activiti.engine.impl.interceptor.CommandContext; +import org.activiti.engine.impl.persistence.entity.SignalEventSubscriptionEntity; + +/** + * An {@link ActivitiEventListener} that throws a signal event when an event is + * dispatched to it. + * + * @author Frederik Heremans + * + */ +public class SignalThrowingEventListener extends BaseDelegateEventListener { + + protected String signalName; + protected boolean processInstanceScope = true; + + @Override + public void onEvent(ActivitiEvent event) { + if (isValidEvent(event)) { + + if (event.getProcessInstanceId() == null && processInstanceScope) { + throw new ActivitiIllegalArgumentException( + "Cannot throw process-instance scoped signal, since the dispatched event is not part of an ongoing process instance"); + } + + CommandContext commandContext = Context.getCommandContext(); + List subscriptionEntities = null; + if (processInstanceScope) { + subscriptionEntities = commandContext.getEventSubscriptionEntityManager() + .findSignalEventSubscriptionsByProcessInstanceAndEventName(event.getProcessInstanceId(), signalName); + } else { + subscriptionEntities = commandContext.getEventSubscriptionEntityManager() + .findSignalEventSubscriptionsByEventName(signalName); + } + + for (SignalEventSubscriptionEntity signalEventSubscriptionEntity : subscriptionEntities) { + signalEventSubscriptionEntity.eventReceived(null, false); + } + } + } + + public void setSignalName(String signalName) { + this.signalName = signalName; + } + + public void setProcessInstanceScope(boolean processInstanceScope) { + this.processInstanceScope = processInstanceScope; + } + + @Override + public boolean isFailOnException() { + return true; + } +} diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/DefaultListenerFactory.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/DefaultListenerFactory.java index 50e4175229b05b72e12c850754642728c5d816f6..55d437fa5bbbc05fca5a2ce670c4ceff67b5e2f4 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/DefaultListenerFactory.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/DefaultListenerFactory.java @@ -12,19 +12,36 @@ */ package org.activiti.engine.impl.bpmn.parser.factory; +import java.util.HashMap; +import java.util.Map; + import org.activiti.bpmn.model.ActivitiListener; import org.activiti.bpmn.model.EventListener; +import org.activiti.bpmn.model.ImplementationType; +import org.activiti.engine.ActivitiIllegalArgumentException; import org.activiti.engine.delegate.ExecutionListener; import org.activiti.engine.delegate.TaskListener; import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.impl.bpmn.helper.BaseDelegateEventListener; import org.activiti.engine.impl.bpmn.helper.ClassDelegate; import org.activiti.engine.impl.bpmn.helper.DelegateActivitiEventListener; import org.activiti.engine.impl.bpmn.helper.DelegateExpressionActivitiEventListener; +import org.activiti.engine.impl.bpmn.helper.ErrorThrowingEventListener; +import org.activiti.engine.impl.bpmn.helper.MessageThrowingEventListener; +import org.activiti.engine.impl.bpmn.helper.SignalThrowingEventListener; import org.activiti.engine.impl.bpmn.listener.DelegateExpressionExecutionListener; import org.activiti.engine.impl.bpmn.listener.DelegateExpressionTaskListener; import org.activiti.engine.impl.bpmn.listener.ExpressionExecutionListener; import org.activiti.engine.impl.bpmn.listener.ExpressionTaskListener; import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.activiti.engine.repository.ProcessDefinition; +import org.activiti.engine.runtime.Execution; +import org.activiti.engine.runtime.Job; +import org.activiti.engine.runtime.ProcessInstance; +import org.activiti.engine.task.Attachment; +import org.activiti.engine.task.Comment; +import org.activiti.engine.task.IdentityLink; +import org.activiti.engine.task.Task; /** * Default implementation of the {@link ListenerFactory}. @@ -34,7 +51,19 @@ import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl; * @author Joram Barrez */ public class DefaultListenerFactory extends AbstractBehaviorFactory implements ListenerFactory { - + + public static final Map> ENTITY_MAPPING = new HashMap>(); + static { + ENTITY_MAPPING.put("attachment", Attachment.class); + ENTITY_MAPPING.put("comment", Comment.class); + ENTITY_MAPPING.put("execution", Execution.class); + ENTITY_MAPPING.put("identity-link", IdentityLink.class); + ENTITY_MAPPING.put("job", Job.class); + ENTITY_MAPPING.put("process-definition", ProcessDefinition.class); + ENTITY_MAPPING.put("process-instance", ProcessInstance.class); + ENTITY_MAPPING.put("task", Task.class); + } + public TaskListener createClassDelegateTaskListener(ActivitiListener activitiListener) { return new ClassDelegate(activitiListener.getImplementation(), createFieldDeclarations(activitiListener.getFieldExtensions())); } @@ -63,12 +92,56 @@ public class DefaultListenerFactory extends AbstractBehaviorFactory implements L @Override public ActivitiEventListener createClassDelegateEventListener(EventListener eventListener) { - return new DelegateActivitiEventListener(eventListener.getImplementation()); + return new DelegateActivitiEventListener(eventListener.getImplementation(), getEntityType(eventListener.getEntityType())); } @Override public ActivitiEventListener createDelegateExpressionEventListener(EventListener eventListener) { return new DelegateExpressionActivitiEventListener(expressionManager.createExpression( - eventListener.getImplementation())); + eventListener.getImplementation()), getEntityType(eventListener.getEntityType())); + } + + @Override + public ActivitiEventListener createEventThrowingEventListener(EventListener eventListener) { + BaseDelegateEventListener result = null; + if (ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT.equals(eventListener.getImplementationType())) { + result = new SignalThrowingEventListener(); + ((SignalThrowingEventListener) result).setSignalName(eventListener.getImplementation()); + ((SignalThrowingEventListener) result).setProcessInstanceScope(true); + } else if (ImplementationType.IMPLEMENTATION_TYPE_THROW_GLOBAL_SIGNAL_EVENT.equals(eventListener.getImplementationType())) { + result = new SignalThrowingEventListener(); + ((SignalThrowingEventListener) result).setSignalName(eventListener.getImplementation()); + ((SignalThrowingEventListener) result).setProcessInstanceScope(false); + } else if (ImplementationType.IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT.equals(eventListener.getImplementationType())) { + result = new MessageThrowingEventListener(); + ((MessageThrowingEventListener) result).setMessageName(eventListener.getImplementation()); + } else if (ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT.equals(eventListener.getImplementationType())) { + result = new ErrorThrowingEventListener(); + ((ErrorThrowingEventListener) result).setErrorCode(eventListener.getImplementation()); + } + + if (result == null) { + throw new ActivitiIllegalArgumentException("Cannot create an event-throwing event-listener, unknown implementation type: " + + eventListener.getImplementationType()); + } + + result.setEntityClass(getEntityType(eventListener.getEntityType())); + return result; + } + + /** + * @param entityType the name of the entity + * @return + * @throws ActivitiIllegalArgumentException when the given entity name + */ + protected Class getEntityType(String entityType) { + if(entityType != null) { + Class entityClass = ENTITY_MAPPING.get(entityType.trim()); + if(entityClass == null) { + throw new ActivitiIllegalArgumentException("Unsupported entity-type for an ActivitiEventListener: " + entityType); + } + return entityClass; + } + return null; } } diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/ListenerFactory.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/ListenerFactory.java index e6c8320fe5e574f66923e36cd3c11f221f8eb07c..4f9fbd7f761a5cf7c0b0ad2f068f707a026bd872 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/ListenerFactory.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/factory/ListenerFactory.java @@ -53,5 +53,7 @@ public interface ListenerFactory { public abstract ActivitiEventListener createClassDelegateEventListener(EventListener eventListener); public abstract ActivitiEventListener createDelegateExpressionEventListener(EventListener eventListener); + + public abstract ActivitiEventListener createEventThrowingEventListener(EventListener eventListener); } \ No newline at end of file diff --git a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/handler/ProcessParseHandler.java b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/handler/ProcessParseHandler.java index a55f5b597807505d8267c5fff60a36ab3e4af305..9c9d6823788b1838f92f5a7331814bd8eabac183 100644 --- a/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/handler/ProcessParseHandler.java +++ b/modules/activiti-engine/src/main/java/org/activiti/engine/impl/bpmn/parser/handler/ProcessParseHandler.java @@ -110,6 +110,12 @@ public class ProcessParseHandler extends AbstractBpmnParseHandler { } else if(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION.equals(eventListener.getImplementationType())) { currentProcessDefinition.getEventSupport().addEventListener( bpmnParse.getListenerFactory().createDelegateExpressionEventListener(eventListener), types); + } else if(ImplementationType.IMPLEMENTATION_TYPE_THROW_SIGNAL_EVENT.equals(eventListener.getImplementationType()) + || ImplementationType.IMPLEMENTATION_TYPE_THROW_GLOBAL_SIGNAL_EVENT.equals(eventListener.getImplementationType()) + || ImplementationType.IMPLEMENTATION_TYPE_THROW_MESSAGE_EVENT.equals(eventListener.getImplementationType()) + || ImplementationType.IMPLEMENTATION_TYPE_THROW_ERROR_EVENT.equals(eventListener.getImplementationType())){ + currentProcessDefinition.getEventSupport().addEventListener( + bpmnParse.getListenerFactory().createEventThrowingEventListener(eventListener), types); } else { bpmnParse.getBpmnModel().addProblem( "Unsupported implementation type for EventLIstener: " + eventListener.getImplementationType(), diff --git a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ActivityEventsTest.java b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ActivityEventsTest.java index 4b88f40aa3dc87dac8c8212df06e2dd0aeff41de..db911b7128f9f7410af4eb8477c7fbc72f29fdd2 100644 --- a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ActivityEventsTest.java +++ b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ActivityEventsTest.java @@ -15,6 +15,7 @@ package org.activiti.engine.test.api.event; import java.util.Collections; import org.activiti.engine.delegate.event.ActivitiActivityEvent; +import org.activiti.engine.delegate.event.ActivitiErrorEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventType; import org.activiti.engine.delegate.event.ActivitiMessageEvent; @@ -345,8 +346,85 @@ public class ActivityEventsTest extends PluggableActivitiTestCase { assertEquals(processInstance.getProcessDefinitionId(), signalEvent.getProcessDefinitionId()); assertEquals("compensationDone", signalEvent.getSignalName()); assertNull(signalEvent.getSignalData()); + + // Check if the process is still alive + processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(processInstance.getId()) + .singleResult(); + + assertNotNull(processInstance); + } + + /** + * Test events related to error-events + */ + @Deployment + public void testActivityErrorEvents() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("errorProcess"); + assertNotNull(processInstance); + + // Error-handling should have ended the process + ProcessInstance afterErrorInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNull(afterErrorInstance); + + ActivitiErrorEvent errorEvent = null; + + for(ActivitiEvent event : listener.getEventsReceived()) { + if(event instanceof ActivitiErrorEvent) { + if(errorEvent == null) { + errorEvent = (ActivitiErrorEvent) event; + } else { + fail("Only one ActivityErrorEvent expected"); + } + } + } + + assertNotNull(errorEvent); + assertEquals(ActivitiEventType.ACTIVITY_ERROR_RECEIVED, errorEvent.getType()); + assertEquals("catchError", errorEvent.getActivityId()); + assertEquals("123", errorEvent.getErrorCode()); + assertEquals(processInstance.getId(), errorEvent.getProcessInstanceId()); + assertEquals(processInstance.getProcessDefinitionId(), errorEvent.getProcessDefinitionId()); + assertFalse(processInstance.getId().equals(errorEvent.getExecutionId())); } + + /** + * Test events related to error-events, thrown from within process-execution (eg. service-task). + */ + @Deployment + public void testActivityErrorEventsFromBPMNError() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("errorProcess"); + assertNotNull(processInstance); + + // Error-handling should have ended the process + ProcessInstance afterErrorInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNull(afterErrorInstance); + + ActivitiErrorEvent errorEvent = null; + + for(ActivitiEvent event : listener.getEventsReceived()) { + if(event instanceof ActivitiErrorEvent) { + if(errorEvent == null) { + errorEvent = (ActivitiErrorEvent) event; + } else { + fail("Only one ActivityErrorEvent expected"); + } + } + } + + assertNotNull(errorEvent); + assertEquals(ActivitiEventType.ACTIVITY_ERROR_RECEIVED, errorEvent.getType()); + assertEquals("catchError", errorEvent.getActivityId()); + assertEquals("23", errorEvent.getErrorCode()); + assertEquals(processInstance.getId(), errorEvent.getProcessInstanceId()); + assertEquals(processInstance.getProcessDefinitionId(), errorEvent.getProcessDefinitionId()); + assertFalse(processInstance.getId().equals(errorEvent.getExecutionId())); + } + + @Override protected void initializeServices() { diff --git a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.java b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..754f5c76a6d2a5743fb59352ca0b8a291f02ce0d --- /dev/null +++ b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.java @@ -0,0 +1,146 @@ +/* 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.activiti.engine.test.api.event; + +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.delegate.event.ActivitiEventType; +import org.activiti.engine.impl.bpmn.helper.ErrorThrowingEventListener; +import org.activiti.engine.impl.test.PluggableActivitiTestCase; +import org.activiti.engine.runtime.ProcessInstance; +import org.activiti.engine.task.Task; +import org.activiti.engine.test.Deployment; + +/** + * Test case for all {@link ActivitiEventListener}s that throws an error BPMN event when an {@link ActivitiEvent} + * has been dispatched. + * + * @author Frederik Heremans + */ +public class ErrorThrowingEventListenerTest extends PluggableActivitiTestCase { + + @Deployment + public void testThrowError() throws Exception { + ErrorThrowingEventListener listener = null; + try { + listener = new ErrorThrowingEventListener(); + + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testError"); + assertNotNull(processInstance); + + // Fetch the task and assign it. Should cause error-event to be dispatched + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("userTask") + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Error-handling should have been called, and "escalate" task should be available instead of original one + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("escalatedTask") + .singleResult(); + assertNotNull(task); + + + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + @Deployment + public void testThrowErrorDefinedInProcessDefinition() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testError"); + assertNotNull(processInstance); + + // Fetch the task and assign it. Should cause error-event to be dispatched + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("userTask") + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Error-handling should have been called, and "escalate" task should be available instead of original one + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("escalatedTask") + .singleResult(); + assertNotNull(task); + } + + @Deployment + public void testThrowErrorWithErrorcode() throws Exception { + ErrorThrowingEventListener listener = null; + try { + listener = new ErrorThrowingEventListener(); + listener.setErrorCode("123"); + + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testError"); + assertNotNull(processInstance); + + // Fetch the task and assign it. Should cause error-event to be dispatched + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("userTask") + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Error-handling should have been called, and "escalate" task should be available instead of original one + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("escalatedTask") + .singleResult(); + assertNotNull(task); + + // Try with a different error-code, resulting in a different task being created + listener.setErrorCode("456"); + + processInstance = runtimeService.startProcessInstanceByKey("testError"); + assertNotNull(processInstance); + + // Fetch the task and assign it. Should cause error-event to be dispatched + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("userTask") + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("escalatedTask2") + .singleResult(); + assertNotNull(task); + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + @Deployment + public void testThrowErrorWithErrorcodeDefinedInProcessDefinition() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testError"); + assertNotNull(processInstance); + + // Fetch the task and assign it. Should cause error-event to be dispatched + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("userTask") + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Error-handling should have been called, and "escalate" task should be available instead of original one + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("escalatedTask") + .singleResult(); + assertNotNull(task); + } +} diff --git a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.java b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1c94345d97b7847f263b2306584d8ce590fc2cb3 --- /dev/null +++ b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.java @@ -0,0 +1,127 @@ +/* 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.activiti.engine.test.api.event; + +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.delegate.event.ActivitiEventType; +import org.activiti.engine.impl.bpmn.helper.MessageThrowingEventListener; +import org.activiti.engine.impl.test.PluggableActivitiTestCase; +import org.activiti.engine.runtime.ProcessInstance; +import org.activiti.engine.task.Task; +import org.activiti.engine.test.Deployment; + +/** + * Test case for all {@link ActivitiEventListener}s that throw a message BPMN event when an {@link ActivitiEvent} + * has been dispatched. + * + * @author Frederik Heremans + */ +public class MessageThrowingEventListenerTest extends PluggableActivitiTestCase { + + @Deployment + public void testThrowMessage() throws Exception { + MessageThrowingEventListener listener = null; + try { + listener = new MessageThrowingEventListener(); + listener.setMessageName("Message"); + + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMessage"); + assertNotNull(processInstance); + + // Fetch the task and re-assig it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been messaged and a new task should be available, on top of the already + // existing one, since the cancelActivity='false' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + + + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + @Deployment + public void testThrowMessageDefinedInProcessDefinition() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMessage"); + assertNotNull(processInstance); + + // Fetch the task and re-assig it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been messaged and a new task should be available, on top of the already + // existing one, since the cancelActivity='false' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + } + + @Deployment + public void testThrowMessageInterrupting() throws Exception { + MessageThrowingEventListener listener = null; + try { + listener = new MessageThrowingEventListener(); + listener.setMessageName("Message"); + + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMessage"); + assertNotNull(processInstance); + + // Fetch the task and re-assig it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been messaged and a new task should be available, the already + // existing one should be removed, since the cancelActivity='true' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNull(task); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } +} diff --git a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.java b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..160b995de8bb356c983a351af48ce3082edc32a2 --- /dev/null +++ b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.java @@ -0,0 +1,305 @@ +/* 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.activiti.engine.test.api.event; + +import org.activiti.engine.ActivitiException; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventListener; +import org.activiti.engine.delegate.event.ActivitiEventType; +import org.activiti.engine.impl.bpmn.helper.SignalThrowingEventListener; +import org.activiti.engine.impl.test.PluggableActivitiTestCase; +import org.activiti.engine.runtime.Job; +import org.activiti.engine.runtime.ProcessInstance; +import org.activiti.engine.task.Task; +import org.activiti.engine.test.Deployment; + +/** + * Test case for all {@link ActivitiEventListener}s that throws a signal BPMN event when an {@link ActivitiEvent} + * has been dispatched. + * + * @author Frederik Heremans + */ +public class SignalThrowingEventListenerTest extends PluggableActivitiTestCase { + + + @Deployment + public void testThrowSignal() throws Exception { + SignalThrowingEventListener listener = null; + try { + listener = new SignalThrowingEventListener(); + listener.setSignalName("Signal"); + listener.setProcessInstanceScope(true); + + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testSignal"); + assertNotNull(processInstance); + + // Fetch the task and re-assign it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been signaled and a new task should be available, on top of the already + // existing one, since the cancelActivity='false' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + + + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + @Deployment + public void testThrowSignalDefinedInProcessDefinition() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testSignal"); + assertNotNull(processInstance); + + // Fetch the task and re-assign it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been signaled and a new task should be available, on top of the already + // existing one, since the cancelActivity='false' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + } + + @Deployment + public void testThrowSignalInterrupting() throws Exception { + SignalThrowingEventListener listener = null; + try { + listener = new SignalThrowingEventListener(); + listener.setSignalName("Signal"); + listener.setProcessInstanceScope(true); + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testSignal"); + assertNotNull(processInstance); + + // Fetch the task and re-assig it to trigger the event-listener + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + taskService.setAssignee(task.getId(), "kermit"); + + // Boundary-event should have been signalled and a new task should be available, the already + // existing one is gone, since the cancelActivity='true' + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("subTask") + .singleResult(); + assertNull(task); + + Task boundaryTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .taskDefinitionKey("boundaryTask") + .singleResult(); + assertNotNull(boundaryTask); + + + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + /** + * Test signal throwing when a job failed and the retries are decremented, affectively + * starting a new transaction. + */ + @Deployment + public void testThrowSignalInNewTransaction() throws Exception { + SignalThrowingEventListener listener = null; + try { + listener = new SignalThrowingEventListener(); + listener.setSignalName("Signal"); + listener.setProcessInstanceScope(true); + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.JOB_RETRIES_DECREMENTED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testSignal"); + assertNotNull(processInstance); + + waitForJobExecutorToProcessAllJobs(2000, 100); + + Job failedJob = managementService.createJobQuery() + .withException() + .processInstanceId(processInstance.getId()) + + .singleResult(); + assertNotNull(failedJob); + assertEquals(0, failedJob.getRetries()); + + // Three retries should each have triggered dispatching of a retry-decrement event + assertEquals(3, taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()); + + try { + managementService.executeJob(failedJob.getId()); + fail("Exception expected"); + } catch(ActivitiException ae) { + // Ignore, expected exception + assertEquals(4, taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()); + } + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + /** + * Test signal throwing when a job failed, signaling will happen in the rolled back transaction, + * not doing anything in the end... + */ + @Deployment(resources = {"org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInNewTransaction.bpmn20.xml"}) + public void testThrowSignalInRolledbackTransaction() throws Exception { + SignalThrowingEventListener listener = null; + + try { + listener = new SignalThrowingEventListener(); + listener.setSignalName("Signal"); + listener.setProcessInstanceScope(true); + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.JOB_EXECUTION_FAILURE); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testSignal"); + assertNotNull(processInstance); + + waitForJobExecutorToProcessAllJobs(2000, 100); + + Job failedJob = managementService.createJobQuery() + .withException() + .processInstanceId(processInstance.getId()) + + .singleResult(); + assertNotNull(failedJob); + assertEquals(0, failedJob.getRetries()); + + // Three retries should each have triggered dispatching of a retry-decrement event + assertEquals(0, taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()); + + try { + managementService.executeJob(failedJob.getId()); + fail("Exception expected"); + } catch(ActivitiException ae) { + // Ignore, expected exception + assertEquals(0, taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()); + } + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } + + /** + * Test if an engine-wide signal is thrown as response to a dispatched event. + */ + @Deployment(resources = { + "org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignal.bpmn20.xml", + "org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalExternalProcess.bpmn20.xml" + }) + public void testGlobalSignal() throws Exception { + SignalThrowingEventListener listener = null; + + try { + listener = new SignalThrowingEventListener(); + listener.setSignalName("Signal"); + listener.setProcessInstanceScope(false); + processEngineConfiguration.getEventDispatcher().addEventListener(listener, ActivitiEventType.TASK_ASSIGNED); + + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("globalSignalProcess"); + assertNotNull(processInstance); + + ProcessInstance externalProcess = runtimeService.startProcessInstanceByKey("globalSignalProcessExternal"); + assertNotNull(processInstance); + // Make sure process is not ended yet by querying it again + externalProcess = runtimeService.createProcessInstanceQuery().processInstanceId(externalProcess.getId()) + .singleResult(); + assertNotNull(externalProcess); + + + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + + // Assign task to trigger signal + taskService.setAssignee(task.getId(), "kermit"); + + // Second process should have been signaled + externalProcess = runtimeService.createProcessInstanceQuery().processInstanceId(externalProcess.getId()) + .singleResult(); + assertNull(externalProcess); + + // Task assignee should still be set + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + + } finally { + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + + } + + /** + * Test if an engine-wide signal is thrown as response to a dispatched event. + */ + @Deployment(resources = { + "org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalDefinedInProcessDefinition.bpmn20.xml", + "org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalExternalProcess.bpmn20.xml" + }) + public void testGlobalSignalDefinedInProcessDefinition() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("globalSignalProcess"); + assertNotNull(processInstance); + + ProcessInstance externalProcess = runtimeService.startProcessInstanceByKey("globalSignalProcessExternal"); + assertNotNull(processInstance); + // Make sure process is not ended yet by querying it again + externalProcess = runtimeService.createProcessInstanceQuery().processInstanceId(externalProcess.getId()) + .singleResult(); + assertNotNull(externalProcess); + + + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + + // Assign task to trigger signal + taskService.setAssignee(task.getId(), "kermit"); + + // Second process should have been signaled + externalProcess = runtimeService.createProcessInstanceQuery().processInstanceId(externalProcess.getId()) + .singleResult(); + assertNull(externalProcess); + + // Task assignee should still be set + task = taskService.createTaskQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNotNull(task); + assertEquals("kermit", task.getAssignee()); + } +} diff --git a/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/UncaughtErrorEventTest.java b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/UncaughtErrorEventTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4f1281434fafc13d32ae1c15ded05cc73159dcaa --- /dev/null +++ b/modules/activiti-engine/src/test/java/org/activiti/engine/test/api/event/UncaughtErrorEventTest.java @@ -0,0 +1,121 @@ +/* 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.activiti.engine.test.api.event; + +import org.activiti.engine.delegate.event.ActivitiErrorEvent; +import org.activiti.engine.delegate.event.ActivitiEvent; +import org.activiti.engine.delegate.event.ActivitiEventType; +import org.activiti.engine.impl.test.PluggableActivitiTestCase; +import org.activiti.engine.runtime.ProcessInstance; +import org.activiti.engine.test.Deployment; + +/** + * Test case for {@link ActivitiEvent} thrown when a BPMNError is not caught + * in the process. + * + * @author Frederik Heremans + */ +public class UncaughtErrorEventTest extends PluggableActivitiTestCase { + + private TestActivitiEventListener listener; + + /** + * Test events related to error-events, thrown from within process-execution (eg. service-task). + */ + @Deployment + public void testUncaughtError() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("errorProcess"); + assertNotNull(processInstance); + + // Error-handling should have ended the process + ProcessInstance afterErrorInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNull(afterErrorInstance); + + ActivitiEvent errorEvent = null; + + for(ActivitiEvent event : listener.getEventsReceived()) { + if(ActivitiEventType.UNCAUGHT_BPMN_ERROR.equals(event.getType())) { + if(errorEvent == null) { + errorEvent = event; + } else { + fail("Only one ActivityErrorEvent expected"); + } + } + } + + assertNotNull(errorEvent); + assertEquals(ActivitiEventType.UNCAUGHT_BPMN_ERROR, errorEvent.getType()); + assertTrue(errorEvent instanceof ActivitiErrorEvent); + assertEquals("123", ((ActivitiErrorEvent) errorEvent).getErrorCode()); + assertNull(((ActivitiErrorEvent) errorEvent).getActivityId()); + assertEquals(processInstance.getId(), errorEvent.getProcessInstanceId()); + assertEquals(processInstance.getProcessDefinitionId(), errorEvent.getProcessDefinitionId()); + assertFalse(processInstance.getId().equals(errorEvent.getExecutionId())); + } + + /** + * Test events related to error-events, thrown from within process-execution (eg. service-task). + */ + @Deployment + public void testUncaughtErrorFromBPMNError() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("errorProcess"); + assertNotNull(processInstance); + + // Error-handling should have ended the process + ProcessInstance afterErrorInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()) + .singleResult(); + assertNull(afterErrorInstance); + + ActivitiEvent errorEvent = null; + + for(ActivitiEvent event : listener.getEventsReceived()) { + if(ActivitiEventType.UNCAUGHT_BPMN_ERROR.equals(event.getType())) { + if(errorEvent == null) { + errorEvent = event; + } else { + fail("Only one ActivityErrorEvent expected"); + } + } + } + + assertNotNull(errorEvent); + assertEquals(ActivitiEventType.UNCAUGHT_BPMN_ERROR, errorEvent.getType()); + assertTrue(errorEvent instanceof ActivitiErrorEvent); + assertEquals("23", ((ActivitiErrorEvent) errorEvent).getErrorCode()); + assertNull(((ActivitiErrorEvent) errorEvent).getActivityId()); + assertEquals(processInstance.getId(), errorEvent.getProcessInstanceId()); + assertEquals(processInstance.getProcessDefinitionId(), errorEvent.getProcessDefinitionId()); + assertFalse(processInstance.getId().equals(errorEvent.getExecutionId())); + } + + + + @Override + protected void initializeServices() { + super.initializeServices(); + + listener = new TestActivitiEventListener(); + processEngineConfiguration.getEventDispatcher().addEventListener(listener); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + if (listener != null) { + listener.clearEventsReceived(); + processEngineConfiguration.getEventDispatcher().removeEventListener(listener); + } + } +} diff --git a/modules/activiti-engine/src/test/java/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.java b/modules/activiti-engine/src/test/java/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.java index 6d2fd26e96774e1a648842a758d7c862cda51eac..5d0e0ea1f05e4d8cda2664df776f23a2a3fedb52 100644 --- a/modules/activiti-engine/src/test/java/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.java +++ b/modules/activiti-engine/src/test/java/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.java @@ -17,10 +17,11 @@ import java.util.List; import org.activiti.engine.ActivitiClassLoadingException; import org.activiti.engine.ActivitiException; import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.delegate.event.ActivitiEntityEvent; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.ActivitiEventType; -import org.activiti.engine.delegate.event.ActivitiEntityEvent; import org.activiti.engine.impl.test.ResourceActivitiTestCase; +import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; import org.activiti.engine.test.Deployment; @@ -60,9 +61,15 @@ public class ProcessDefinitionScopedEventListenerDefinitionTest extends Resource assertEquals(ActivitiEventType.ENTITY_CREATED, event.getType()); } - // First event received should be creation of Process-instance + // First event received should be creation of Process-definition assertTrue(testListenerBean.getEventsReceived().get(0) instanceof ActivitiEntityEvent); ActivitiEntityEvent event = (ActivitiEntityEvent) testListenerBean.getEventsReceived().get(0); + assertTrue(event.getEntity() instanceof ProcessDefinition); + assertEquals(processInstance.getProcessDefinitionId(), ((ProcessDefinition) event.getEntity()).getId()); + + // First event received should be creation of Process-instance + assertTrue(testListenerBean.getEventsReceived().get(0) instanceof ActivitiEntityEvent); + event = (ActivitiEntityEvent) testListenerBean.getEventsReceived().get(1); assertTrue(event.getEntity() instanceof ProcessInstance); assertEquals(processInstance.getId(), ((ProcessInstance) event.getEntity()).getId()); @@ -88,14 +95,12 @@ public class ProcessDefinitionScopedEventListenerDefinitionTest extends Resource * Test to verify listeners defined in the BPMN xml with invalid class/delegateExpression * values cause an exception when process is started. */ - @Deployment(resources = { - "org/activiti/standalone/event/invalidEventListenerClass.bpmn20.xml", - "org/activiti/standalone/event/invalidEventListenerExpression.bpmn20.xml"}) public void testProcessDefinitionListenerDefinitionError() throws Exception { - // Start process with expression which references an unexisting bean + // Deploy process with expression which references an unexisting bean try { - runtimeService.startProcessInstanceByKey("testInvalidEventExpression"); + repositoryService.createDeployment().addClasspathResource("org/activiti/standalone/event/invalidEventListenerExpression.bpmn20.xml") + .deploy(); fail("Exception expected"); } catch(ActivitiException ae) { assertEquals("Exception while executing event-listener", ae.getMessage()); @@ -103,9 +108,10 @@ public class ProcessDefinitionScopedEventListenerDefinitionTest extends Resource assertEquals("Unknown property used in expression: ${unexistingBean}", ae.getCause().getMessage()); } - // Start process with listener which references an unexisting class + // Deploy process with listener which references an unexisting class try { - runtimeService.startProcessInstanceByKey("testInvalidEventClass"); + repositoryService.createDeployment().addClasspathResource("org/activiti/standalone/event/invalidEventListenerClass.bpmn20.xml") + .deploy(); fail("Exception expected"); } catch(ActivitiException ae) { assertEquals("Exception while executing event-listener", ae.getMessage()); @@ -142,6 +148,27 @@ public class ProcessDefinitionScopedEventListenerDefinitionTest extends Resource } } + /** + * Test to verify listeners defined in the BPMN xml are added to the process + * definition and are active, for all entity types + */ + @Deployment + public void testProcessDefinitionListenerDefinitionEntities() throws Exception { + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testEventListeners"); + assertNotNull(processInstance); + Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult(); + assertNotNull(task); + + // Attachment entity + TestActivitiEventListener theListener = (TestActivitiEventListener) processEngineConfiguration.getBeans().get("testAttachmentEventListener"); + assertNotNull(theListener); + assertEquals(0, theListener.getEventsReceived().size()); + + taskService.createAttachment("test", task.getId(), processInstance.getId(), "test", "test", "url"); + assertEquals(1, theListener.getEventsReceived().size()); + + } + @Override protected void setUp() throws Exception { super.setUp(); diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEvents.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEvents.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..b920f520655ae3c2e325ec625a61bb9d61c1f65e --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEvents.bpmn20.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEventsFromBPMNError.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEventsFromBPMNError.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..5efa2dc1762875c9dd5410259009c9ee587ebb27 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ActivityEventsTest.testActivityErrorEventsFromBPMNError.bpmn20.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowError.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowError.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..74e9cbeac64b8ca324cad3bbaf8ca48ef6b6e06b --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowError.bpmn20.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorDefinedInProcessDefinition.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorDefinedInProcessDefinition.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0c6c9e2869cb9c8206bdc55a54b3e90fd31e3d2 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorDefinedInProcessDefinition.bpmn20.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcode.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcode.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..f69140873d4eb24a70754d8a65fa7ac36038c10c --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcode.bpmn20.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcodeDefinedInProcessDefinition.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcodeDefinedInProcessDefinition.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..dac3c7a36ae7da2e67097f43a952f07390629584 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/ErrorThrowingEventListenerTest.testThrowErrorWithErrorcodeDefinedInProcessDefinition.bpmn20.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessage.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessage.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..276cb0c5373152aaee610d62afed3bfe2928a2fc --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessage.bpmn20.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageDefinedInProcessDefinition.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageDefinedInProcessDefinition.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..45dd4e24914cdd05b1e12d38c4188b44061337f3 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageDefinedInProcessDefinition.bpmn20.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageInterrupting.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageInterrupting.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..faf5a55c75b054ecfdccf8905ff14c8c1f67de1d --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/MessageThrowingEventListenerTest.testThrowMessageInterrupting.bpmn20.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignal.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignal.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..5155405b4555bce132d9439a09ecfc430490950f --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignal.bpmn20.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalDefinedInProcessDefinition.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalDefinedInProcessDefinition.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..e74164cf89907f3d3e98bd0a5bebae937f5b9035 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalDefinedInProcessDefinition.bpmn20.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalExternalProcess.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalExternalProcess.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..9814853839c919814207665723da4800e5f2e3cb --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.globalSignalExternalProcess.bpmn20.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignal.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignal.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..ca52161bfe2f3d847e869ae86d07c8bd68107b95 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignal.bpmn20.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalDefinedInProcessDefinition.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalDefinedInProcessDefinition.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..f7aa35605d1f2e482be44cc182cee0482a6d2b07 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalDefinedInProcessDefinition.bpmn20.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInNewTransaction.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInNewTransaction.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..97b331417feddd8d1ffd8a4f592fec43eb6ba72d --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInNewTransaction.bpmn20.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInterrupting.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInterrupting.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..3a0a59e80cad158a266365bf25999522446872db --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/SignalThrowingEventListenerTest.testThrowSignalInterrupting.bpmn20.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtError.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtError.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..763f9b522d70fb8b9f60e336ce4d18f7782578df --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtError.bpmn20.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtErrorFromBPMNError.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtErrorFromBPMNError.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..f3d3a967507e37bfa2e1ce8bae091060f26eeef0 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/engine/test/api/event/UncaughtErrorEventTest.testUncaughtErrorFromBPMNError.bpmn20.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.testProcessDefinitionListenerDefinitionEntities.bpmn20.xml b/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.testProcessDefinitionListenerDefinitionEntities.bpmn20.xml new file mode 100644 index 0000000000000000000000000000000000000000..61a4ec3244883e7d47d3fda8bbc77c1838fc54f5 --- /dev/null +++ b/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/ProcessDefinitionScopedEventListenerDefinitionTest.testProcessDefinitionListenerDefinitionEntities.bpmn20.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/activiti-eventlistener.cfg.xml b/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/activiti-eventlistener.cfg.xml index a4c3e1e906b622e6dfce4c43102b213e7cc82c14..83dc449256773e982efcafe0cfdedf7e898f779a 100644 --- a/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/activiti-eventlistener.cfg.xml +++ b/modules/activiti-engine/src/test/resources/org/activiti/standalone/event/activiti-eventlistener.cfg.xml @@ -29,6 +29,14 @@ + + + + + + + + @@ -39,4 +47,12 @@ + + + + + + + + diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/service/api/RestResponseFactory.java b/modules/activiti-rest/src/main/java/org/activiti/rest/service/api/RestResponseFactory.java index d83551e70f246358002814ee6e9d1df7dfd7b73d..1ab45181c62875f469f93d47a79273316c179c45 100644 --- a/modules/activiti-rest/src/main/java/org/activiti/rest/service/api/RestResponseFactory.java +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/service/api/RestResponseFactory.java @@ -418,7 +418,7 @@ public class RestResponseFactory { result.setTaskUrl(securedResource.createFullResourceUrl(RestUrls.URL_TASK, attachment.getTaskId())); } if(attachment.getProcessInstanceId() != null) { - result.setTaskUrl(securedResource.createFullResourceUrl(RestUrls.URL_PROCESS_INSTANCE, attachment.getProcessInstanceId())); + result.setProcessInstanceUrl(securedResource.createFullResourceUrl(RestUrls.URL_PROCESS_INSTANCE, attachment.getProcessInstanceId())); } return result ; } diff --git a/userguide/src/en/chapters/ch03-Configuration.xml b/userguide/src/en/chapters/ch03-Configuration.xml index e53664322259a78b02268bcb503315916a8abd24..39048526b9da992da197ac7afbea5d006f1c09b4 100644 --- a/userguide/src/en/chapters/ch03-Configuration.xml +++ b/userguide/src/en/chapters/ch03-Configuration.xml @@ -584,7 +584,7 @@ executionId=%X{mdcExecutionId} mdcProcessInstanceID=%X{mdcProcessInstanceID} mdc
- [API AND NAMING NOT FINAL YET] Event handlers + Event handlers An event mechanism has been introduced in Activiti 5.15. It allows you to get notified when various events occur within the engine. Take a look at all supported event types for an overview of the events available. @@ -728,9 +728,11 @@ void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... Adding listeners to process definitions It's possible to add listeners to a specific process-definition. The listeners will only be called for events related to the process definition and to all events related to process instances that are started with that specific process - definition. The listener implementations can be defined using a fully qualified classname or an expression that resolves to a bean that implements - the listener interface. + definition. The listener implementations can be defined using a fully qualified classname, an expression that resolves to a bean that implements + the listener interface or can be configured to throw a message/signal/error BPMN event. +
+ Listeners executing user-defined logic The snippet below adds 2 listeners to a process-definition. The first listener will receive events of any type, with a listener implementation based on a fully-qualified class name. The second listener is only notified when a job is successfully executed or when it failed, using a listener that has been defined in the beans property of the process engine configuration. @@ -742,9 +744,78 @@ void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... ... -</process> +</process> + For events related to entities, it's also possible to add listeners to a process-definition that get only notified when enitity-events occur for a certain + entity type. The snippet below shows how this can be achieved. It can be used along for ALL entity-events (first example) or for specific event types only (second example). + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener class="org.activiti.engine.test.MyEventListener" entityType="task" /> + <activiti:eventListener delegateExpression="${testEventListener}" events="ENTITY_CREATED" entityType="task" /> + </extensionElements> - Please note that: + ... + +</process> + + For events related to entities, it's also possible to add listeners to a process-definition that get notified only enitity-events occur for a certain + entity type. The snippet below shows how this can be done. It can be used along for ALL entity-events (first example) or for specific event types only (second example). + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener class="org.activiti.engine.test.MyEventListener" entityType="task" /> + <activiti:eventListener delegateExpression="${testEventListener}" events="ENTITY_CREATED" entityType="task" /> + </extensionElements> + + ... + +</process> +Supported values for the entityType are: attachment, comment, execution,identity-link, job, process-instance, +process-definition, task. +
+
+ Listeners throwing BPMN events + + + [EXPERIMENTAL] + + + Another way of handling events being dispatched is to throw a BPMN event. Please bare in mind that it only makes sense to throw BPMN-events with certain kinds of + activiti event types. For example, throwing a BPMN event when the process-instance is deleted will result in an error. The snippet below shows how to throw a signal inside process-instance, throw a signal to an external + process (global), throw a message-event inside the process-instance and throw an error-event inside the process-instance. Instead of using the class or delegateExpression, + the attribute throwEvent is used, along with an additional attribute, specific to the type of event being thrown. + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener throwEvent="signal" signalName="My signal" events="TASK_ASSIGNED" /> + </extensionElements> +</process> + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener throwEvent="globalSignal" signalName="My signal" events="TASK_ASSIGNED" /> + </extensionElements> +</process> + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener throwEvent="message" messageName="My message" events="TASK_ASSIGNED" /> + </extensionElements> +</process> + +<process id="testEventListeners"> + <extensionElements> + <activiti:eventListener throwEvent="error" errorCode="123" events="TASK_ASSIGNED" /> + </extensionElements> +</process> + +If additional logic is needed to decide wether or not to throw the BPMN-event, it's possible to extend the listener-classes provided by Activiti. By overriding the isValidEvent(ActivitiEvent event) +in your subclass, BPMN-event throwing can be prevented. The classes involved are org.activiti.engine.test.api.event.SignalThrowingEventListenerTest, org.activiti.engine.impl.bpmn.helper.MessageThrowingEventListener and org.activiti.engine.impl.bpmn.helper.ErrorThrowingEventListener. + +
+
+ Notes on listeners on a process-definition + @@ -764,12 +835,13 @@ void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... - When an illegal event-type is used in the events attribute, an exception will be thrown when the process-definition is deployed (effectievly failing the deployment). When an illegal value for class or delegateExecution is supplied (either unexisting class, unexisting bean referenced or delegate not implementing listener interface), an exception will + When an illegal event-type is used in the events attribute or illegal throwEvent value is used, an exception will be thrown when the process-definition is deployed (effectievly failing the deployment). When an illegal value for class or delegateExecution is supplied (either unexisting class, unexisting bean referenced or delegate not implementing listener interface), an exception will be thrown when the process is started (or when the first valid event for that process-definition is dispatched to the listener). Make sure the referenced classes are on the classpath and that the expressions resolve to a valid instance. +
Dispatching events through API @@ -879,6 +951,19 @@ void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... be dispatched for this activity, depending on the type (boudnary-event or event-subprocess start-event) org.activiti...ActivitiMessageEvent + + ACTIVITY_ERROR_RECEIVED + An activity has received an error event. Dispatched before the actual error has been handled by + the activity. The event's activityId contains a reference to the error-handling activity. + This event will be either followed by a ACTIVITY_SIGNALLED event or ACTIVITY_COMPLETE + for the involved activity, if the error was delivered successfully. + org.activiti...ActivitiErrorEvent + + + UNCAUGHT_BPMN_ERROR + An uncaught BPMN error has been thrown. The process did not have any handlers for that specific error. The event's activityId will be empty. + org.activiti...ActivitiErrorEvent + ACTIVITY_COMPENSATE An activity is about to be compensated. The event contains the id of the activity that is will be executed for compensation.