提交 3f7f7a54 编写于 作者: F frederikheremans

ACT-125 added event listener + made java serviceTask use shared instance of...

ACT-125 added event listener + made java serviceTask use shared instance of delegate class + field injection altered
上级 48964075
/* 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;
import java.util.List;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.impl.bpmn.parser.FieldDeclaration;
import org.activiti.pvm.event.EventListener;
import org.activiti.pvm.event.EventListenerExecution;
/**
* EventListener that delegates the event notification to an instance of
* {@link EventListener}. The delegate {@link EventListener} instance is only
* created once and injected based on the fieldDeclarations.
*
* @author Frederik Heremans
*/
public class DelegateEventListener extends FieldDeclarationDelegate implements EventListener {
public DelegateEventListener(String className, List<FieldDeclaration> fieldDeclarations) {
super(className, fieldDeclarations);
}
public void notify(EventListenerExecution execution) throws Exception {
Object delegate = getDelegateInstance();
if (delegate instanceof EventListener) {
((EventListener) delegate).notify(execution);
} else {
throw new ActivitiException("Class " + getDelegateClassName() + " is used as event-listener but does not implement the interface "
+ EventListener.class.getName());
}
}
}
/* 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;
import java.lang.reflect.Field;
import java.util.List;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.impl.bpmn.parser.FieldDeclaration;
import org.activiti.engine.impl.util.ReflectUtil;
/**
* Extend this class to add support for using field injection based on {@link FieldDeclaration}s on
* a single object instance of a specific class.
*
* @author Frederik Heremans
*/
public class FieldDeclarationDelegate {
private Object objectInstance;
private String className;
private List<FieldDeclaration> fieldDeclarations;
public FieldDeclarationDelegate(String className, List<FieldDeclaration> fieldDeclarations) {
this.className = className;
this.fieldDeclarations = fieldDeclarations;
}
/**
* Get the delegate object instance that has it's fields injected based on the
* {@link FieldDeclaration}s. The same instance is always returned,
* it is is created and injected only once.
*/
protected Object getDelegateInstance() {
if(objectInstance == null) {
objectInstance = ReflectUtil.instantiate(className);
injectFieldDeclarations(objectInstance);
}
return objectInstance;
}
private void injectFieldDeclarations(Object object) {
if(fieldDeclarations != null) {
for(FieldDeclaration declaration : fieldDeclarations) {
Field field = ReflectUtil.getField(declaration.getName(), object);
if(field == null) {
throw new ActivitiException("Field definition uses unexisting field '" + declaration.getName() + "' on class " + className);
}
// Check if the delegate field's type is correct
if(!fieldTypeCompatible(declaration, field)) {
throw new ActivitiException("Incompatible type set on field declaration '" + declaration.getName() +
"' for class " + className + ". Declared value has type " + declaration.getValue().getClass().getName() + ", while expecting " +
field.getType().getName());
}
ReflectUtil.setField(field, object, declaration.getValue());
}
}
}
private boolean fieldTypeCompatible(FieldDeclaration declaration, Field field) {
if(declaration.getValue() != null) {
return field.getType().isAssignableFrom(declaration.getValue().getClass());
} else {
// Null can be set any field type
return true;
}
}
protected List<FieldDeclaration> getFieldDeclarations() {
return fieldDeclarations;
}
protected String getDelegateClassName() {
return className;
}
}
......@@ -16,6 +16,8 @@ package org.activiti.engine.impl.bpmn;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.bpmn.BpmnJavaDelegation;
import org.activiti.engine.impl.cfg.ProcessEngineConfiguration;
import org.activiti.engine.impl.el.ActivitiMethodExpression;
import org.activiti.engine.impl.el.ActivitiValueExpression;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.pvm.delegate.DelegateExecution;
import org.apache.commons.mail.Email;
......@@ -23,48 +25,56 @@ import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
/**
* @author Joram Barrez
* @author Frederik Heremans
*/
public class MailActivityBehavior extends BpmnJavaDelegation {
private String to;
private String from;
private String cc;
private String bcc;
private String subject;
private String text;
private String html;
private Object to;
private Object from;
private Object cc;
private Object bcc;
private Object subject;
private Object text;
private Object html;
public void execute(DelegateExecution execution) {
Email email = createEmail();
addTo(email);
setFrom(email);
addCc(email);
addBcc(email);
setSubject(email);
String toStr = getStringFromField(to, execution);
String fromStr = getStringFromField(from, execution);
String ccStr = getStringFromField(cc, execution);
String bccStr = getStringFromField(bcc, execution);
String subjectStr = getStringFromField(subject, execution);
String textStr = getStringFromField(text, execution);
String htmlStr = getStringFromField(html, execution);
Email email = createEmail(textStr, htmlStr);
addTo(email, toStr);
setFrom(email, fromStr);
addCc(email, ccStr);
addBcc(email, bccStr);
setSubject(email, subjectStr);
setMailServerProperties(email);
try {
email.send();
} catch (EmailException e) {
throw new ActivitiException("Could not send e-mail", e);
}
}
protected Email createEmail() {
protected Email createEmail(String text, String html) {
if (html != null) {
return createHtmlEmail();
return createHtmlEmail(text, html);
} else if (text != null) {
return createTextOnlyEmail();
return createTextOnlyEmail(text);
} else {
throw new ActivitiException("'html' or 'text' is required to be defined when using the mail activity");
}
}
protected HtmlEmail createHtmlEmail() {
protected HtmlEmail createHtmlEmail(String text, String html) {
HtmlEmail email = new HtmlEmail();
try {
email.setHtmlMsg(html);
......@@ -76,8 +86,8 @@ public class MailActivityBehavior extends BpmnJavaDelegation {
throw new ActivitiException("Could not create HTML email", e);
}
}
protected SimpleEmail createTextOnlyEmail() {
protected SimpleEmail createTextOnlyEmail(String text) {
SimpleEmail email = new SimpleEmail();
try {
email.setMsg(text);
......@@ -86,8 +96,8 @@ public class MailActivityBehavior extends BpmnJavaDelegation {
throw new ActivitiException("Could not create text-only email", e);
}
}
protected void addTo(Email email) {
protected void addTo(Email email, String to) {
String[] tos = splitAndTrim(to);
if (tos != null) {
for (String t : tos) {
......@@ -101,24 +111,24 @@ public class MailActivityBehavior extends BpmnJavaDelegation {
throw new ActivitiException("No recipient could be found for sending email");
}
}
protected void setFrom(Email email) {
protected void setFrom(Email email, String from) {
String fromAddres = null;
if (this.from != null) {
if (from != null) {
fromAddres = from;
} else { // use default configured from address in process engine config
fromAddres = CommandContext.getCurrent().getProcessEngineConfiguration().getMailServerDefaultFrom();
}
try {
email.setFrom(fromAddres);
} catch (EmailException e) {
throw new ActivitiException("Could not set " + from + " as from address in email", e);
}
}
protected void addCc(Email email) {
protected void addCc(Email email, String cc) {
String[] ccs = splitAndTrim(cc);
if (ccs != null) {
for (String c : ccs) {
......@@ -130,52 +140,77 @@ public class MailActivityBehavior extends BpmnJavaDelegation {
}
}
}
protected void addBcc(Email email) {
protected void addBcc(Email email, String bcc) {
String[] bccs = splitAndTrim(bcc);
if (bccs != null) {
for (String b : bccs) {
try {
email.addBcc(b);
} catch (EmailException e) {
throw new ActivitiException("Could not add " + b+ " as bcc recipient", e);
throw new ActivitiException("Could not add " + b + " as bcc recipient", e);
}
}
}
}
protected void setSubject(Email email) {
protected void setSubject(Email email, String subject) {
email.setSubject(subject != null ? subject : "");
}
protected void setMailServerProperties(Email email) {
ProcessEngineConfiguration config = CommandContext.getCurrent().getProcessEngineConfiguration();
String host = config.getMailServerSmtpHost();
if (host == null) {
throw new ActivitiException("Could not send email: no SMTP host is configured");
}
email.setHostName(host);
int port = config.getMailServerSmtpPort();
email.setSmtpPort(port);
String user = config.getMailServerSmtpUserName();
String password = config.getMailServerSmtpPassword();
if (user != null && password != null) {
email.setAuthentication(user, password);
}
}
protected String[] splitAndTrim(String str) {
if (str != null) {
String[] splittedStrings = str.split(",");
for (int i=0; i<splittedStrings.length; i++) {
for (int i = 0; i < splittedStrings.length; i++) {
splittedStrings[i] = splittedStrings[i].trim();
}
return splittedStrings;
}
return null;
}
protected String getStringFromField(Object fieldValue, DelegateExecution execution) {
// TODO: Move to shared location maybe?? Boiler-plate! or wrap in dedicated
// model to allow field
// injection to be transparent to user?
String result = null;
if (fieldValue instanceof String) {
result = (String) fieldValue;
} else if (fieldValue instanceof ActivitiValueExpression) {
Object value = ((ActivitiValueExpression) fieldValue).getValue(execution);
if (value != null) {
result = value.toString();
}
} else if (fieldValue instanceof ActivitiValueExpression) {
Object methodResult = ((ActivitiMethodExpression) fieldValue).invoke(execution);
if (methodResult != null) {
result = methodResult.toString();
}
} else if (fieldValue != null) {
// For non-null objects use toString
result = fieldValue.toString();
}
return result;
}
}
/* 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;
import org.activiti.engine.impl.el.ActivitiMethodExpression;
import org.activiti.pvm.event.EventListener;
import org.activiti.pvm.event.EventListenerExecution;
/**
* An {@link EventListener} that invokes a {@link ActivitiMethodExpression}.
*
* @author Frederik Heremans
*/
public class MethodExpressionEventListener implements EventListener {
protected ActivitiMethodExpression activitiMethodExpression;
public MethodExpressionEventListener(ActivitiMethodExpression activitiMethodExpression) {
this.activitiMethodExpression = activitiMethodExpression;
}
public void notify(EventListenerExecution execution) throws Exception {
activitiMethodExpression.invoke(execution);
}
}
......@@ -13,13 +13,10 @@
package org.activiti.engine.impl.bpmn;
import java.lang.reflect.Field;
import java.util.List;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.impl.bpmn.parser.FieldDeclaration;
import org.activiti.engine.impl.el.ActivitiValueExpression;
import org.activiti.engine.impl.util.ReflectUtil;
import org.activiti.pvm.activity.ActivityBehavior;
import org.activiti.pvm.activity.ActivityExecution;
import org.activiti.pvm.activity.SignallableActivityBehavior;
......@@ -30,80 +27,29 @@ import org.activiti.pvm.activity.SignallableActivityBehavior;
* @author Tom Baeyens
* @author Joram Barrez
*/
public class ServiceTaskDelegateActivityBehaviour implements SignallableActivityBehavior {
public class ServiceTaskDelegateActivityBehaviour extends FieldDeclarationDelegate implements SignallableActivityBehavior {
protected ActivitiValueExpression expression;
protected Class<? extends ActivityBehavior> activityBehaviorType;
protected List<FieldDeclaration> fieldDeclarations;
public ServiceTaskDelegateActivityBehaviour(ActivitiValueExpression expression, List<FieldDeclaration> fieldDeclarations) {
this.expression = expression;
this.fieldDeclarations = fieldDeclarations;
public ServiceTaskDelegateActivityBehaviour(String className, List<FieldDeclaration> fieldDeclarations) {
super(className, fieldDeclarations);
}
public ServiceTaskDelegateActivityBehaviour(Class<? extends ActivityBehavior> activityBehaviorType, List<FieldDeclaration> fieldDeclarations) {
this.activityBehaviorType = activityBehaviorType;
this.fieldDeclarations = fieldDeclarations;
}
public void execute(ActivityExecution execution) throws Exception {
Object object = null;
if (activityBehaviorType != null) {
object = activityBehaviorType.newInstance();
} else {
object = expression.getValue(execution);
// Classname is provided as String
if (object instanceof String) {
String className = (String) object;
if (className != null) {
object = ReflectUtil.instantiate(className);
}
}
}
if (object instanceof ActivityBehavior) {
ActivityBehavior activityBehavior = (ActivityBehavior) object;
injectFields(execution, object);
Object delegate = getDelegateInstance();
if (delegate instanceof ActivityBehavior) {
ActivityBehavior activityBehavior = (ActivityBehavior) delegate;
activityBehavior.execute(execution);
} else {
throw new ActivitiException("Service " + object + " is used in a serviceTask, but does not" + " implement the "
throw new ActivitiException("Service " + delegate + " is used in a serviceTask, but does not" + " implement the "
+ ActivityBehavior.class.getCanonicalName() + " interface");
}
}
private void injectFields(ActivityExecution execution, Object object) {
if (fieldDeclarations != null) {
for (FieldDeclaration fieldDeclaration : fieldDeclarations) {
Field field = ReflectUtil.getField(fieldDeclaration.getName(), object);
if (field == null) {
throw new ActivitiException("Invalid field declaration " + fieldDeclaration.getName()
+ " not found on" + object.getClass().getCanonicalName());
}
// Only String is supported now, so nothing special needs to happen (conversion etc.)
ReflectUtil.setField(field, object, fieldDeclaration.getValueExpression().getValue(execution));
}
}
}
public void signal(ActivityExecution execution, String signalName, Object signalData) throws Exception {
Object object = expression.getValue(execution);
if (object instanceof String) {
String className = (String) object;
if (className != null) {
object = ReflectUtil.instantiate(className);
}
}
if (object instanceof SignallableActivityBehavior) {
((SignallableActivityBehavior) object).signal(execution, signalName, signalData);
Object delegate = getDelegateInstance();
if (delegate instanceof SignallableActivityBehavior) {
((SignallableActivityBehavior) delegate).signal(execution, signalName, signalData);
} else {
throw new ActivitiException("Service " + object + " is used in a serviceTask, but does not" + " implement the "
throw new ActivitiException("Service of type '" + getDelegateClassName() + "' is used in a serviceTask, but does not" + " implement the "
+ SignallableActivityBehavior.class.getCanonicalName() + " interface");
}
}
......
......@@ -13,7 +13,6 @@
package org.activiti.engine.impl.bpmn.parser;
import org.activiti.engine.impl.el.ActivitiValueExpression;
/**
......@@ -22,17 +21,18 @@ import org.activiti.engine.impl.el.ActivitiValueExpression;
* &lt;field name='someField&gt; &lt;string ...
*
* @author Joram Barrez
* @author Frederik Heremans
*/
public class FieldDeclaration {
protected String name;
protected String type;
protected ActivitiValueExpression valueExpression;
protected Object value;
public FieldDeclaration(String name, String type, ActivitiValueExpression valueExpression) {
public FieldDeclaration(String name, String type, Object value) {
this.name = name;
this.type = type;
this.valueExpression = valueExpression;
this.value = value;
}
public FieldDeclaration() {
......@@ -51,11 +51,11 @@ public class FieldDeclaration {
public void setType(String type) {
this.type = type;
}
public ActivitiValueExpression getValueExpression() {
return valueExpression;
public Object getValue() {
return value;
}
public void setValue(ActivitiValueExpression valueExpression) {
this.valueExpression = valueExpression;
public void setValue(Object value) {
this.value = value;
}
}
......@@ -129,7 +129,7 @@ public class EmailServiceTaskTest extends ActivitiInternalTestCase {
List<WiserMessage> messages = wiser.getMessages();
assertEquals(1, messages.size());
assertEmailSend(messages.get(0), true, "Test", "<b>Kermit</b>", "noreply@activiti.org", Arrays.asList("kermit@activiti.org"), null);
assertEmailSend(messages.get(0), true, "Test", "Mr. <b>Kermit</b>", "noreply@activiti.org", Arrays.asList("kermit@activiti.org"), null);
}
// Helper
......
/* 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.examples.bpmn.eventlistener;
import java.util.HashMap;
import java.util.Map;
import org.activiti.engine.impl.test.ActivitiInternalTestCase;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.Deployment;
/**
* @author Frederik Heremans
*/
public class EventListenerTest extends ActivitiInternalTestCase {
@Deployment(resources = {"org/activiti/examples/bpmn/eventlistener/EventListenersProcess.bpmn20.xml"})
public void testEventListenersOnAllPossibleElements() {
// Process start event-listener will have event-listener class that sets 2 variables
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("eventListenersProcess");
String varSetInEventListener = (String) runtimeService.getVariable(processInstance.getId(), "variableSetInEventListener");
String eventNameReceived = (String) runtimeService.getVariable(processInstance.getId(), "eventNameReceived");
assertNotNull(varSetInEventListener);
assertEquals("firstValue", varSetInEventListener);
assertNotNull(eventNameReceived);
assertEquals("start", eventNameReceived);
// Transition take event-listener will set 2 variables
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
assertNotNull(task);
taskService.complete(task.getId());
varSetInEventListener = (String) runtimeService.getVariable(processInstance.getId(), "variableSetInEventListener");
eventNameReceived = (String) runtimeService.getVariable(processInstance.getId(), "eventNameReceived");
assertNotNull(varSetInEventListener);
assertEquals("secondValue", varSetInEventListener);
assertNotNull(eventNameReceived);
assertEquals("take", eventNameReceived);
ExampleEventListenerPojo myPojo = new ExampleEventListenerPojo();
runtimeService.setVariable(processInstance.getId(), "myPojo", myPojo);
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
assertNotNull(task);
taskService.complete(task.getId());
// First usertask uses a method-expression as event-listener: ${myPojo.myMethod(execution.eventName)}
ExampleEventListenerPojo pojoVariable = (ExampleEventListenerPojo) runtimeService.getVariable(processInstance.getId(), "myPojo");
assertNotNull(pojoVariable.getReceivedEventName());
assertEquals("end", pojoVariable.getReceivedEventName());
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
assertNotNull(task);
taskService.complete(task.getId());
assertProcessEnded(processInstance.getId());
}
@Deployment(resources = {"org/activiti/examples/bpmn/eventlistener/EventListenersFieldInjectionProcess.bpmn20.xml"})
public void testEventListenerFieldInjection() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("myVar", "listening!");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("eventListenersProcess", variables);
Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
assertNotNull(varSetByListener);
assertTrue(varSetByListener instanceof String);
// Result is a concatenation of fixed injected field and injected value-expression
assertEquals("Yes, I am listening!", varSetByListener);
}
}
/* 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.examples.bpmn.eventlistener;
import org.activiti.pvm.event.EventListener;
import org.activiti.pvm.event.EventListenerExecution;
/**
* Simple {@link EventListener} that sets 2 variables on the execution.
*
* @author Frederik Heremans
*/
public class ExampleEventListenerOne implements EventListener {
public void notify(EventListenerExecution execution) throws Exception {
execution.setVariable("variableSetInEventListener", "firstValue");
execution.setVariable("eventNameReceived", execution.getEventName());
}
}
/* 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.examples.bpmn.eventlistener;
import java.io.Serializable;
/**
* Simple pojo than will be used to act as an event listener.
*
* @author Frederik Heremans
*/
public class ExampleEventListenerPojo implements Serializable {
private static final long serialVersionUID = 1L;
private String receivedEventName;
public void myMethod(String eventName) {
this.receivedEventName = eventName;
}
public String getReceivedEventName() {
return receivedEventName;
}
public void setReceivedEventName(String receivedEventName) {
this.receivedEventName = receivedEventName;
}
}
/* 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.examples.bpmn.eventlistener;
import org.activiti.pvm.event.EventListener;
import org.activiti.pvm.event.EventListenerExecution;
/**
* Simple {@link EventListener} that sets 2 variables on the execution.
*
* @author Frederik Heremans
*/
public class ExampleEventListenerTwo implements EventListener {
public void notify(EventListenerExecution execution) throws Exception {
execution.setVariable("variableSetInEventListener", "secondValue");
execution.setVariable("eventNameReceived", execution.getEventName());
}
}
/* 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.examples.bpmn.eventlistener;
import org.activiti.engine.impl.el.ActivitiValueExpression;
import org.activiti.pvm.event.EventListener;
import org.activiti.pvm.event.EventListenerExecution;
/**
* Example {@link EventListener} which gets 2 fields injected
* @author Frederik Heremans
*/
public class ExampleFieldInjectedEventListener implements EventListener {
private String fixedValue;
private ActivitiValueExpression dynamicValue;
@Override
public void notify(EventListenerExecution execution) throws Exception {
execution.setVariable("var", fixedValue + dynamicValue.getValue(execution).toString());
}
}
/* 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.examples.bpmn.servicetask;
import java.io.Serializable;
/**
* Simple class for testing purposes.
*
* @author Frederik Heremans
*/
public class GenderBean implements Serializable {
private static final long serialVersionUID = 1L;
public String getGenderString(String gender) {
return "Your gender is: " + gender;
}
}
......@@ -52,12 +52,16 @@ public class JavaServiceTaskTest extends ActivitiInternalTestCase {
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("name", "kermit");
vars.put("gender", "male");
vars.put("genderBean", new GenderBean());
ProcessInstance pi = runtimeService.startProcessInstanceByKey("expressionFieldInjection", vars);
Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(pi.getId())
.activityId("waitState")
.singleResult();
assertEquals("HELLO MR. KERMIT", runtimeService.getVariable(execution.getId(), "var"));
assertEquals("timrek .rM olleH", runtimeService.getVariable(execution.getId(), "var2"));
assertEquals("elam :si redneg ruoY", runtimeService.getVariable(execution.getId(), "var1"));
}
public void testIllegalUseOfResultVariableName() {
......
/* 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.examples.bpmn.servicetask;
import org.activiti.engine.bpmn.BpmnJavaDelegation;
import org.activiti.engine.impl.el.ActivitiMethodExpression;
import org.activiti.engine.impl.el.ActivitiValueExpression;
import org.activiti.pvm.delegate.DelegateExecution;
/**
* Example BpmnJavaDelegation that uses an injected
* {@link ActivitiMethodExpression} and {@link ActivitiValueExpression} in fields
* 'text1' and 'text2'. While executing, 'var1' is set with the reversed result of the
* method invocation and 'var2' will be the reversed result of the value expression.
*
* @author Frederik Heremans
*/
public class ReverseStringsFieldInjected extends BpmnJavaDelegation {
private ActivitiMethodExpression text1;
private ActivitiValueExpression text2;
public void execute(DelegateExecution execution) {
String value1 = (String) text1.invoke(execution);
execution.setVariable("var1", new StringBuffer(value1).reverse().toString());
String value2 = (String) text2.getValue(execution);
execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
}
}
......@@ -18,7 +18,7 @@
<activiti:string>Test</activiti:string>
</activiti:field>
<activiti:field activiti:name="html">
<activiti:string>
<activiti:valueExpr>
<![CDATA[
<html>
<body>
......@@ -26,7 +26,7 @@
</body>
</html>
]]>
</activiti:string>
</activiti:valueExpr>
</activiti:field>
</extensionElements>
</serviceTask>
......
......@@ -12,16 +12,16 @@
<serviceTask id="sendMail" activiti:type="mail">
<extensionElements>
<activiti:field activiti:name="from">
<activiti:string>${sender}</activiti:string>
<activiti:valueExpr>${sender}</activiti:valueExpr>
</activiti:field>
<activiti:field activiti:name="to">
<activiti:string>${recipient}</activiti:string>
<activiti:valueExpr>${recipient}</activiti:valueExpr>
</activiti:field>
<activiti:field activiti:name="subject">
<activiti:string>${subject}</activiti:string>
<activiti:valueExpr>${subject}</activiti:valueExpr>
</activiti:field>
<activiti:field activiti:name="text">
<activiti:string>Hello ${recipientName}, this is an e-mail</activiti:string>
<activiti:valueExpr>Hello ${recipientName}, this is an e-mail</activiti:valueExpr>
</activiti:field>
</extensionElements>
</serviceTask>
......
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn-extensions"
targetNamespace="http://www.activiti.org/bpmn2.0">
<process id="eventListenersProcess">
<extensionElements>
<activiti:listener activiti:class="org.activiti.examples.bpmn.eventlistener.ExampleFieldInjectedEventListener" activiti:eventName="start">
<activiti:field activiti:name="fixedValue" activiti:stringValue="Yes, I am " />
<activiti:field activiti:name="dynamicValue" activiti:valueExpr="${myVar}" />
</activiti:listener>
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
</definitions>
\ No newline at end of file
<definitions
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn-extensions"
targetNamespace="http://www.activiti.org/bpmn2.0">
<process id="eventListenersProcess">
<extensionElements>
<activiti:listener activiti:class="org.activiti.examples.bpmn.eventlistener.ExampleEventListenerOne" activiti:eventName="start" />
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="secondTask">
<extensionElements>
<activiti:listener activiti:class="org.activiti.examples.bpmn.eventlistener.ExampleEventListenerTwo" />
</extensionElements>
</sequenceFlow>
<userTask id="secondTask" >
<extensionElements>
<activiti:listener activiti:methodExpr="${myPojo.myMethod(execution.eventName)}" activiti:eventName="end" />
</extensionElements>
</userTask>
<sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />
<userTask id="thirdTask" />
<sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
</definitions>
\ No newline at end of file
......@@ -11,11 +11,11 @@
<serviceTask id="sendMail" activiti:type="mail">
<extensionElements>
<activiti:field activiti:name="from" activiti:string-value="${sender}" />
<activiti:field activiti:name="to" activiti:string-value="${recipient}" />
<activiti:field activiti:name="subject" activiti:string-value="Your order ${orderId} has been shipped" />
<activiti:field activiti:name="from" activiti:valueExpr="${sender}" />
<activiti:field activiti:name="to" activiti:valueExpr="${recipient}" />
<activiti:field activiti:name="subject" activiti:valueExpr="Your order ${orderId} has been shipped" />
<activiti:field activiti:name="html">
<activiti:string>
<activiti:valueExpr>
<![CDATA[
<html>
<body>
......@@ -29,7 +29,7 @@
</body>
</html>
]]>
</activiti:string>
</activiti:valueExpr>
</activiti:field>
</extensionElements>
</serviceTask>
......
......@@ -13,10 +13,13 @@
<serviceTask id="javaService"
name="Java service invocation"
activiti:class="org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
activiti:class="org.activiti.examples.bpmn.servicetask.ReverseStringsFieldInjected">
<extensionElements>
<activiti:field activiti:name="text">
<activiti:string>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</activiti:string>
<activiti:field activiti:name="text1">
<activiti:methodExpr>${genderBean.getGenderString(gender)}</activiti:methodExpr>
</activiti:field>
<activiti:field activiti:name="text2">
<activiti:valueExpr>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</activiti:valueExpr>
</activiti:field>
</extensionElements>
</serviceTask>
......
......@@ -1564,56 +1564,114 @@ public class ToUppercase extends BpmnJavaDelegation {
}
</programlisting>
</para>
<para>An important thing to note is that there will be <emphasis role="bold">only one instance of that Java class created for the serviceTask it is defined on</emphasis>.
All process-instances share the same class instance that will be used to call <emphasis>execute(DelegateExecution)</emphasis>.
This means that the class must not use any member variables and must be thread-safe, since it can be executed simultaneously from different threads.
This also influences the way <link linkend="serviceTaskFieldInjection">Field injection</link> is handled.
</para>
</section>
<section id="serviceTaskFieldInjection">
<title>Field injection</title>
<para>
It's possible to inject values into the fields of the delegated classes.
Currently, only <emphasis>String</emphasis> injection is supported. The following
code snippet shows how to inject a constant value into a field. Field injection
is supported when using the <emphasis>'class''</emphasis> attribute.
Note that we need to <emphasis role="bold">declare a 'extensionElements' XML element
before the actual field injection declarations</emphasis>, which is a requirement of
the BPMN 2.0 XML Schema.
<programlisting>
<para>
It's possible to inject values into the fields of the delegated classes. The folowing types of injection are supported:
<itemizedlist>
<listitem><para>Fixed string values.</para></listitem>
<listitem><para>Value expressions.</para></listitem>
<listitem><para>Method expressions.</para></listitem>
</itemizedlist>
</para>
<para>
The following code snippet shows how to inject a constant value into a field. Field injection
is supported when using the <emphasis>'class''</emphasis> attribute.
Note that we need to <emphasis role="bold">declare a 'extensionElements' XML element
before the actual field injection declarations</emphasis>, which is a requirement of
the BPMN 2.0 XML Schema.
<programlisting>
&lt;serviceTask id=&quot;javaService&quot;
name=&quot;Java service invocation&quot;
activiti:class=&quot;org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected&quot;&gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:field activiti:name=&quot;text&quot; activiti:string-value=&quot;Hello World&quot; /&gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:field activiti:name=&quot;text&quot; activiti:stringValue=&quot;Hello World&quot; /&gt;
&lt;/extensionElements&gt;</emphasis>
&lt;/serviceTask&gt;
</programlisting>
Alternatively, for longs texts (e.g. an inline e-mail) the <emphasis>'activiti:string'</emphasis>
sub element can be used:
<programlisting>
</programlisting>
Alternatively, for longs texts (e.g. an inline e-mail) the <emphasis>'activiti:string'</emphasis>
sub element can be used:
<programlisting>
&lt;serviceTask id=&quot;javaService&quot;
name=&quot;Java service invocation&quot;
activiti:class=&quot;org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected&quot;&gt;
&lt;extensionElements&gt;
&lt;activiti:field activiti:name=&quot;text&quot;&gt;
<emphasis role="bold">&lt;activiti:string&gt;
<emphasis role="bold">&lt;activiti:string&gt;
Hello World
&lt;/activiti:string&gt;</emphasis>
&lt;/activiti:field&gt;
&lt;/extensionElements&gt;
&lt;/serviceTask&gt;
</programlisting>
</para>
<para>
To inject values that are dynamically resolved at runtime, expressions can be used.
Those expressions can use process variables, or Spring defined beans (if Spring is used):
<programlisting>
&lt;activiti:field activiti:name=&quot;text&quot;&gt;
&lt;activiti:string&gt;Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}&lt;/activiti:string&gt;
&lt;/activiti:field&gt;
</programlisting>
</para>
<para>
To inject values that are dynamically resolved at runtime, expressions can be used. Those expressions can use process variables, or Spring defined beans (if Spring is used).
As noted in <link linkend="bpmnJavaServiceTaskImplementation">Service Task Implementation</link>, an instance of the Java class is shared among all process-instances in a service task.
To have dynamic injection of values in fields, you can inject <literal>org.activiti.engine.impl.el.ActivitiValueExpression</literal>s and <literal>org.activiti.engine.impl.el.ActivitiMethodExpression</literal>s,
which can be evaluated/invoked using the <literal>DelegateExecution</literal> passed in the <literal>execute</literal> method.
<programlisting>
&lt;serviceTask id=&quot;javaService&quot; name=&quot;Java service invocation&quot;
activiti:class=&quot;org.activiti.examples.bpmn.servicetask.ReverseStringsFieldInjected&quot;&gt;
&lt;extensionElements&gt;
&lt;activiti:field activiti:name=&quot;text1&quot;&gt;
<emphasis role="bold">&lt;activiti:methodExpr&gt;${genderBean.getGenderString(gender)}&lt;/activiti:methodExpr&gt;</emphasis>
&lt;/activiti:field&gt;
&lt;activiti:field activiti:name=&quot;text2&quot;&gt;
<emphasis role="bold">&lt;activiti:valueExpr&gt;Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}&lt;/activiti:valueExpr&gt;</emphasis>
&lt;/activiti:field&gt;
&lt;/ extensionElements&gt;
&lt;/ serviceTask&gt;
</programlisting>
</para>
</para>
<para>
The example class below uses the injected expressions and resolves them using the current <literal>DelegateExecution</literal>.
Full code and test can be found in <literal>org.activiti.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection</literal>
<programlisting>
public class ReverseStringsFieldInjected extends BpmnJavaDelegation {
private ActivitiMethodExpression text1;
private ActivitiValueExpression text2;
public void execute(DelegateExecution execution) {
String value1 = (String) text1.invoke(execution);
execution.setVariable("var1", new StringBuffer(value1).reverse().toString());
String value2 = (String) text2.getValue(execution);
execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
}
}
</programlisting>
</para>
<para>
Alternatively, you can also set the expressions as an attribute instead of a child-element, to make the XML less verbose.
<programlisting>
&lt;activiti:field activiti:name=&quot;text1&quot; <emphasis role="bold">activiti:methodExpr=&quot;${genderBean.getGenderString(gender)}&quot;</emphasis> /&gt;
&lt;activiti:field activiti:name=&quot;text1&quot; <emphasis role="bold">activiti:valueExpr=&quot;Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}&quot;</emphasis> /&gt;
</programlisting>
</para>
<para>
<emphasis role="bold">
Since the Java class instance is reused, the injection only happens once, when the serviceTask is called the first time. When the fields are altered by your code,
the values won't be re-injected so you should treat them as immutable and don't make any changes to them.
</emphasis>
</para>
</section>
......@@ -1632,15 +1690,153 @@ public class ToUppercase extends BpmnJavaDelegation {
/&gt;
</programlisting>
In the above example, the result of the service execution (the return value of the <emphasis>'doSomething()'</emphasis> method invocation on an object that is made available
In the example above, the result of the service execution (the return value of the <emphasis>'doSomething()'</emphasis> method invocation on an object that is made available
under the name <emphasis>'myService'</emphasis> either in the process variables or as a Spring bean) is set to the process variable named <emphasis>'myVar'</emphasis> after the service execution completes.
</para>
</section>
</section>
<section id="eventListeners">
<title>Event listeners</title>
<para>Event listeners allow you to execute external Java code or execute a method expression when certain events occur. The events that can be captured are:
<itemizedlist>
<listitem><para>Start and ending of a process instance.</para></listitem>
<listitem><para>Taking a transition.</para></listitem>
<listitem><para>Start and ending of an activity.</para></listitem>
</itemizedlist>
</para>
<para>
The folowing process definition contains 3 event-listeners:
<programlisting>
&lt;process id=&quot;eventListenersProcess&quot;&gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:listener activiti:class=&quot;org.activiti.examples.bpmn.eventlistener.ExampleEventListenerOne&quot; activiti:eventName=&quot;start&quot; /&gt;
&lt;/extensionElements&gt;</emphasis>
&lt;startEvent id=&quot;theStart&quot; /&gt;
&lt;sequenceFlow sourceRef=&quot;theStart&quot; targetRef=&quot;firstTask&quot; /&gt;
&lt;userTask id=&quot;firstTask&quot; /&gt;
&lt;sequenceFlow sourceRef=&quot;firstTask&quot; targetRef=&quot;secondTask&quot;&gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:listener activiti:class=&quot;org.activiti.examples.bpmn.eventlistener.ExampleEventListenerTwo&quot; /&gt;
&lt;/extensionElements&gt;</emphasis>
&lt;/sequenceFlow&gt;
&lt;userTask id=&quot;secondTask&quot; &gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:listener activiti:methodExpr=&quot;${myPojo.myMethod(execution.eventName)}&quot; activiti:eventName=&quot;end&quot; /&gt;
&lt;/extensionElements&gt;</emphasis>
&lt;/userTask&gt;
&lt;sequenceFlow sourceRef=&quot;secondTask&quot; targetRef=&quot;thirdTask&quot; /&gt;
&lt;userTask id=&quot;thirdTask&quot; /&gt;
&lt;sequenceFlow sourceRef=&quot;thirdTask&quot; targetRef=&quot;theEnd&quot; /&gt;
&lt;endEvent id=&quot;theEnd&quot; /&gt;
&lt;/process&gt;</programlisting>
</para>
<para>The first event-listener is notified when the process starts. The listener is an external Java-class (<literal>ExampleEventListenerOne</literal>) which implements
<literal>org.activiti.pvm.event.EventListener</literal>. Classes implementing <literal>org.activiti.engine.bpmn.BpmnJavaDelegation</literal>
can be also used as eventlistener and have the advantage that they can also be used within a service task. When the event occurs (in this case <literal>end</literal>)
the method <literal>notify(EventListenerExecution execution)</literal> is called.
<programlisting>
public class ExampleEventListenerOne implements <emphasis role="bold">EventListener</emphasis> {
<emphasis role="bold">public void notify(EventListenerExecution execution) throws Exception {</emphasis>
execution.setVariable("variableSetInEventListener", "firstValue");
execution.setVariable("eventNameReceived", execution.getEventName());
}
}</programlisting>
</para>
<para>
The second event-listener is called when the transition is taken. Note that the <literal>listener</literal> element doesn't define an
<literal>eventName</literal>, since only <literal>take</literal> events are fired on transitions.
<emphasis role="bold">Values in the <literal>eventName</literal> attribute are ignored when a listener is defined on a transition.</emphasis>
</para>
<para>
The last eventlistener is called when activity <literal>secondTask</literal> ends. Instead of using the <literal>class</literal> on the listener declaration,
a <literal>methodExpression</literal> is defined instead.
</para>
<programlisting>
&lt;activiti:listener activiti:methodExpr=&quot;<emphasis role="bold">${myPojo.myMethod(execution.eventName)}</emphasis>&quot; activiti:eventName=&quot;end&quot; /&gt;</programlisting>
<para>
As with other expressions, execution variables are resolved and can be used. Because an eventlistener gets a special kind of execution, an <literal>EventListenerExecution</literal> that is, it's
possible to pass the event-name to your methods using <literal>execution.eventName</literal>.
</para>
<section id="eventListenerFieldInjection">
<title>Field injection on event listeners</title>
<para>
When using an eventlistener that is configured with the <literal>class</literal> attribute, field injection can be applied. This is exaclty the same
mechanism as used <link linkend="serviceTaskFieldInjection">Service task field injection</link>, which contains an overview of the possibilities provided by field injection.
</para>
<para>
The fragment below shows a simple example process with an event-listener with fields injected.
<programlisting>
&lt;process id=&quot;eventListenersProcess&quot;&gt;
<emphasis role="bold">&lt;extensionElements&gt;
&lt;activiti:listener activiti:class=&quot;org.activiti.examples.bpmn.eventlistener.ExampleFieldInjectedEventListener&quot; activiti:eventName=&quot;start&quot;&gt;
&lt;activiti:field activiti:name=&quot;fixedValue&quot; activiti:stringValue=&quot;Yes, I am &quot; /&gt;
&lt;activiti:field activiti:name=&quot;dynamicValue&quot; activiti:valueExpr=&quot;${myVar}&quot; /&gt;
&lt;/activiti:listener&gt;
&lt;/extensionElements&gt;</emphasis>
&lt;startEvent id=&quot;theStart&quot; /&gt;
&lt;sequenceFlow sourceRef=&quot;theStart&quot; targetRef=&quot;firstTask&quot; /&gt;
&lt;userTask id=&quot;firstTask&quot; /&gt;
&lt;sequenceFlow sourceRef=&quot;firstTask&quot; targetRef=&quot;theEnd&quot; /&gt;
&lt;endEvent id=&quot;theEnd&quot; /&gt;
&lt;/process&gt;
</programlisting>
</para>
<para>
<programlisting>
public class ExampleFieldInjectedEventListener implements EventListener {
<emphasis role="bold">private String fixedValue;
private ActivitiValueExpression dynamicValue;</emphasis>
@Override
public void notify(EventListenerExecution execution) throws Exception {
execution.setVariable(<emphasis role="bold">"var", fixedValue + dynamicValue.getValue(execution).toString()</emphasis>);
}
}
</programlisting>
The class <literal>ExampleFieldInjectedEventListener</literal> concatenates the 2 injected fields (one fixed an the other dynamic) and stores this in the process variable '<literal>var</literal>'.
</para>
<para>
<programlisting>
@Deployment(resources = {"org/activiti/examples/bpmn/eventlistener/EventListenersFieldInjectionProcess.bpmn20.xml"})
public void testEventListenerFieldInjection() {
Map&lt;String, Object&gt; variables = new HashMap&lt;String, Object&gt;();
<emphasis role="bold">variables.put("myVar", "listening!");</emphasis>
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("eventListenersProcess", variables);
Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
assertNotNull(varSetByListener);
assertTrue(varSetByListener instanceof String);
// Result is a concatenation of fixed injected field and injected value-expression
<emphasis role="bold">assertEquals("Yes, I am listening!", varSetByListener);</emphasis>
}
</programlisting>
</para>
</section>
</section>
<section id="bpmnEmailTask">
<section id="bpmnEmailTask">
<title>Email task</title>
......@@ -1788,11 +1984,11 @@ public class ToUppercase extends BpmnJavaDelegation {
<programlisting>
&lt;serviceTask id=&quot;sendMail&quot; activiti:type=&quot;mail&quot;&gt;
&lt;extensionElements&gt;
&lt;activiti:field activiti:name=&quot;from&quot; activiti:string-value=&quot;order-shipping@thecompany.com&quot; /&gt;
&lt;activiti:field activiti:name=&quot;to&quot; activiti:string-value=&quot;${recipient}&quot; /&gt;
&lt;activiti:field activiti:name=&quot;subject&quot; activiti:string-value=&quot;Your order ${orderId} has been shipped&quot; /&gt;
&lt;activiti:field activiti:name=&quot;from&quot; activiti:stringValue=&quot;order-shipping@thecompany.com&quot; /&gt;
&lt;activiti:field activiti:name=&quot;to&quot; activiti:valueExpr=&quot;${recipient}&quot; /&gt;
&lt;activiti:field activiti:name=&quot;subject&quot; activiti:valueExpr=&quot;Your order ${orderId} has been shipped&quot; /&gt;
&lt;activiti:field activiti:name=&quot;html&quot;&gt;
&lt;activiti:string&gt;
&lt;activiti:valueExpr&gt;
&lt;![CDATA[
&lt;html&gt;
&lt;body&gt;
......@@ -1806,7 +2002,7 @@ public class ToUppercase extends BpmnJavaDelegation {
&lt;/body&gt;
&lt;/html&gt;
]]&gt;
&lt;/activiti:string&gt;
&lt;/activiti:valueExpr&gt;
&lt;/activiti:field&gt;
&lt;/extensionElements&gt;
&lt;/serviceTask&gt;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册