/** * Licensed to the Austrian Association for Software Tool Integration (AASTI) * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. The AASTI licenses this file to you 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.openengsb.core.workflow.drools.internal; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.drools.KnowledgeBase; import org.drools.event.process.DefaultProcessEventListener; import org.drools.event.process.ProcessCompletedEvent; import org.drools.event.process.ProcessNodeLeftEvent; import org.drools.event.process.ProcessNodeTriggeredEvent; import org.drools.event.process.ProcessStartedEvent; import org.drools.event.rule.BeforeActivationFiredEvent; import org.drools.event.rule.DefaultAgendaEventListener; import org.drools.impl.KnowledgeBaseImpl; import org.drools.runtime.StatefulKnowledgeSession; import org.drools.runtime.process.NodeInstance; import org.drools.runtime.process.ProcessInstance; import org.drools.runtime.process.WorkflowProcessInstance; import org.drools.runtime.rule.ConsequenceException; import org.drools.runtime.rule.FactHandle; import org.jbpm.workflow.instance.node.SubProcessNodeInstance; import org.openengsb.core.api.Event; import org.openengsb.core.api.EventSupport; import org.openengsb.core.api.context.ContextHolder; import org.openengsb.core.common.AbstractOpenEngSBService; import org.openengsb.core.util.DefaultOsgiUtilsService; import org.openengsb.core.util.OsgiUtils; import org.openengsb.core.util.ThreadLocalUtil; import org.openengsb.core.workflow.api.RemoteEventProcessor; import org.openengsb.core.workflow.api.RuleBaseException; import org.openengsb.core.workflow.api.TaskboxService; import org.openengsb.core.workflow.api.WorkflowException; import org.openengsb.core.workflow.api.WorkflowService; import org.openengsb.core.workflow.api.model.InternalWorkflowEvent; import org.openengsb.core.workflow.api.model.ProcessBag; import org.openengsb.core.workflow.api.model.RemoteEvent; import org.openengsb.core.workflow.api.model.RuleBaseElementId; import org.openengsb.core.workflow.api.model.RuleBaseElementType; import org.openengsb.core.workflow.api.model.Task; import org.openengsb.core.workflow.drools.WorkflowHelper; import org.openengsb.domain.auditing.AuditingDomain; import org.osgi.framework.BundleContext; import org.osgi.framework.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.primitives.Primitives; public class WorkflowServiceImpl extends AbstractOpenEngSBService implements WorkflowService, RemoteEventProcessor { private static final String START_FLOW_CONSEQUENCE_LINE = " )\nthen\n WorkflowHelper.startFlow(kcontext.getKnowledgeRuntime(), \"%s\");\n"; private static final Logger LOGGER = LoggerFactory.getLogger(WorkflowServiceImpl.class); private static final String FLOW_TRIGGER_RULE_TEMPLATE_START = "## This rule has been autogenerated by the WorkflowService\n" + "when\n" + " %s ( name == \"%s\""; private static final String FLOW_TRIGGER_RULE_TEMPLATE_EVENT_FIELD = ", %s == \"%s\""; private DroolsRuleManager rulemanager; private BundleContext bundleContext; private TaskboxService taskbox; private Map<String, StatefulKnowledgeSession> sessions = new HashMap<String, StatefulKnowledgeSession>(); private ExecutorService executor = ThreadLocalUtil.contextAwareExecutor(Executors.newCachedThreadPool()); private Lock workflowLock = new ReentrantLock(); private DefaultOsgiUtilsService utilsService; private Collection<AuditingDomain> auditingConnectors; private Collection<EventSupport> eventReceivers; @Override public void processEvent(Event event) throws WorkflowException { LOGGER.info("processing Event {} of type {}", event, event.getClass()); for (AuditingDomain connector : auditingConnectors) { connector.onEvent(event); } StatefulKnowledgeSession session = getSessionForCurrentContext(); FactHandle factHandle = null; try { factHandle = session.insert(event); workflowLock.lock(); try { session.fireAllRules(); } catch (ConsequenceException e) { throw new WorkflowException("ConsequenceException occured while processing event", e.getCause()); } finally { workflowLock.unlock(); } Set<Long> processIds = retrieveRelevantProcessInstanceIds(event, session); if (processIds.isEmpty()) { for (ProcessInstance p : session.getProcessInstances()) { p.signalEvent(event.getClass().getSimpleName(), event); } } else { signalEventToProcesses(event, session, processIds); } } finally { session.retract(factHandle); } for (EventSupport receiver : eventReceivers) { receiver.onEvent(event); } } @Override public void processRemoteEvent(RemoteEvent event) throws WorkflowException { processEvent(event); } private void signalEventToProcesses(Event event, StatefulKnowledgeSession session, Set<Long> processIds) { for (Long pid : processIds) { ProcessInstance processInstance = session.getProcessInstance(pid); if (processInstance == null) { LOGGER.warn("processInstance with ID {} not found, maybe it already terminated", pid); } else { processInstance.signalEvent(event.getClass().getSimpleName(), event); } } } private Set<Long> retrieveRelevantProcessInstanceIds(Event event, StatefulKnowledgeSession session) { Set<Long> processIds = new HashSet<Long>(); Long processIdFromEvent = event.getProcessId(); if (processIdFromEvent != null) { processIds.add(processIdFromEvent); processIds.addAll(getSubFlows(session.getProcessInstance(processIdFromEvent))); } if (event instanceof InternalWorkflowEvent) { ProcessBag bag = ((InternalWorkflowEvent) event).getProcessBag(); Long processIdFromBag = Long.parseLong(bag.getProcessId()); processIds.add(processIdFromBag); processIds.addAll(getSubFlows(session.getProcessInstance(processIdFromBag))); } return processIds; } private Collection<Long> getSubFlows(ProcessInstance processInstance) { Collection<Long> result = new HashSet<Long>(); if (processInstance == null) { return result; } WorkflowProcessInstance wp = (WorkflowProcessInstance) processInstance; for (NodeInstance n : wp.getNodeInstances()) { if (n instanceof SubProcessNodeInstance) { SubProcessNodeInstance spn = (SubProcessNodeInstance) n; result.add(spn.getProcessInstanceId()); } } return result; } @Override public long startFlow(String processId) throws WorkflowException { return startFlowWithParameters(processId, new HashMap<String, Object>()); } @Override public ProcessBag executeWorkflow(String processId, ProcessBag parameters) throws WorkflowException { Map<String, Object> parameterMap = new HashMap<String, Object>(); parameterMap.put("processBag", parameters); long id = startFlowWithParameters(processId, parameterMap); try { waitForFlowToFinishIndefinitely(id); } catch (InterruptedException e) { throw new WorkflowException(e); } return parameters; } @Override public long startFlowWithParameters(String processId, Map<String, Object> parameterMap) throws WorkflowException { try { return startFlowInBackground(processId, parameterMap).get(); } catch (InterruptedException e) { throw new WorkflowException(e); } catch (ExecutionException e) { throw new WorkflowException("unable to start workflow " + processId, e.getCause()); } } private Future<Long> startFlowInBackground(String processId, Map<String, Object> paramterMap) throws WorkflowException { Callable<Long> call = WorkflowHelper.getCallable(getSessionForCurrentContext(), processId, paramterMap); return executor.submit(call); } @Override public void registerFlowTriggerEvent(Event event, String... flowIds) throws WorkflowException { String eventName = event.getName(); String ruleName = String.format("_generated_ trigger %s on %s", Arrays.asList(flowIds), eventName); StringBuffer ruleCode = generateFlowTriggerRule(event, flowIds); LOGGER.info("adding new rule with id: {}", ruleName); try { rulemanager.add(new RuleBaseElementId(RuleBaseElementType.Rule, ruleName), ruleCode.toString()); } catch (RuleBaseException e) { throw new WorkflowException(e); } } private StringBuffer generateFlowTriggerRule(Event event, String... flowIds) throws WorkflowException { StringBuffer ruleCode = new StringBuffer(); ruleCode.append(String.format(FLOW_TRIGGER_RULE_TEMPLATE_START, event.getClass().getName(), event.getName())); addOtherPropertyChecks(event, ruleCode); for (String flowId : flowIds) { ruleCode.append(String.format(START_FLOW_CONSEQUENCE_LINE, flowId)); } return ruleCode; } private void addOtherPropertyChecks(Event event, StringBuffer ruleCode) throws WorkflowException { Class<? extends Event> eventClass = event.getClass(); List<PropertyDescriptor> properties = reflectPropertiesFromEventClass(eventClass); for (PropertyDescriptor property : properties) { Class<?> propertyType = property.getPropertyType(); if (!propertyType.isPrimitive() && !Primitives.isWrapperType(propertyType) && !propertyType.equals(String.class)) { continue; } Method getter = property.getReadMethod(); if (Modifier.PUBLIC != getter.getModifiers()) { continue; } Object propertyValue = getPropertyValue(event, getter); if (propertyValue == null) { continue; } ruleCode.append(String.format(FLOW_TRIGGER_RULE_TEMPLATE_EVENT_FIELD, property.getName(), propertyValue)); } } private Object getPropertyValue(Event event, Method getter) throws WorkflowException { try { return getter.invoke(event); } catch (Exception e) { throw new WorkflowException("Cannot invoke getter '" + getter + "' of event class '" + event.getClass() + "'.", e); } } private List<PropertyDescriptor> reflectPropertiesFromEventClass(Class<? extends Event> clazz) throws WorkflowException { if (clazz.equals(Event.class)) { return new ArrayList<PropertyDescriptor>(); } try { List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>(); BeanInfo info = Introspector.getBeanInfo(clazz); result.addAll(Arrays.asList(info.getPropertyDescriptors())); BeanInfo eventInfo = Introspector.getBeanInfo(Event.class); result.removeAll(Arrays.asList(eventInfo.getPropertyDescriptors())); return result; } catch (IntrospectionException ie) { throw new WorkflowException("Cannot introspect event class " + clazz, ie); } } @Override public void waitForFlowToFinishIndefinitely(long id) throws InterruptedException, WorkflowException { StatefulKnowledgeSession session = getSessionForCurrentContext(); synchronized (session) { while (session.getProcessInstance(id) != null) { session.wait(); } } } @Override public boolean waitForFlowToFinish(long id, long timeout) throws InterruptedException, WorkflowException { StatefulKnowledgeSession session = getSessionForCurrentContext(); long endTime = System.currentTimeMillis() + timeout; synchronized (session) { while (session.getProcessInstance(id) != null && timeout > 0) { session.wait(timeout); timeout = endTime - System.currentTimeMillis(); } } return !getRunningFlows().contains(id); } @Override public ProcessBag getProcessBagForInstance(long instanceId) { StatefulKnowledgeSession session = getSessionForCurrentContext(); ProcessInstance instance = session.getProcessInstance(instanceId); if (instance == null || !(instance instanceof WorkflowProcessInstance)) { throw new IllegalArgumentException("Process instance with id " + instanceId + " not found"); } return (ProcessBag) ((WorkflowProcessInstance) instance).getVariable("processBag"); } public Collection<Long> getRunningFlows() throws WorkflowException { Collection<ProcessInstance> processInstances = getSessionForCurrentContext().getProcessInstances(); Collection<Long> result = new HashSet<Long>(); for (ProcessInstance p : processInstances) { result.add(p.getId()); } return result; } private StatefulKnowledgeSession getSessionForCurrentContext() throws WorkflowException { String currentContextId = ContextHolder.get().getCurrentContextId(); if (currentContextId == null) { throw new IllegalStateException("contextID must not be null"); } if (sessions.containsKey(currentContextId)) { return sessions.get(currentContextId); } StatefulKnowledgeSession session; try { session = createSession(); } catch (RuleBaseException e) { throw new WorkflowException(e); } sessions.put(currentContextId, session); return session; } protected StatefulKnowledgeSession createSession() throws RuleBaseException, WorkflowException { KnowledgeBase rb = rulemanager.getRulebase(); ((KnowledgeBaseImpl) rb).ruleBase.lock(); LOGGER.debug("retrieved rulebase: {} from source {}", rb, rulemanager); final StatefulKnowledgeSession session = rb.newStatefulKnowledgeSession(); LOGGER.debug("session started"); populateGlobals(session); LOGGER.debug("globals have been set"); session.addEventListener(new DefaultProcessEventListener() { @Override public void beforeNodeTriggered(ProcessNodeTriggeredEvent event) { for (AuditingDomain ac : auditingConnectors) { ProcessInstance instance = event.getProcessInstance(); ac.onNodeStart(instance.getProcessName(), instance.getId(), event.getNodeInstance().getNodeName()); } } @Override public void afterNodeLeft(ProcessNodeLeftEvent event) { for (AuditingDomain ac : auditingConnectors) { ProcessInstance instance = event.getProcessInstance(); ac.onNodeFinish(instance.getProcessName(), instance.getId(), event.getNodeInstance().getNodeName()); } } @Override public void afterProcessCompleted(ProcessCompletedEvent event) { synchronized (session) { session.notifyAll(); } } }); session.addEventListener(new DefaultProcessEventListener() { @Override public void afterProcessStarted(ProcessStartedEvent event) { String processId2 = event.getProcessInstance().getProcessId(); long id = event.getProcessInstance().getId(); LOGGER.info("started process \"{}\". instance-ID: {}", processId2, id); } @Override public void beforeNodeTriggered(ProcessNodeTriggeredEvent event) { long nodeId = event.getNodeInstance().getNodeId(); String nodeName = event.getNodeInstance().getNodeName(); LOGGER.info("Now triggering node \"{}\" (\"{}\").", nodeName, nodeId); } @Override public void afterProcessCompleted(ProcessCompletedEvent event) { String processId2 = event.getProcessInstance().getProcessId(); long id = event.getProcessInstance().getId(); LOGGER.info("process completed \"{}\". instance-ID: {}", processId2, id); } }); session.addEventListener(new DefaultAgendaEventListener() { @Override public void beforeActivationFired(BeforeActivationFiredEvent event) { String ruleName = event.getActivation().getRule().getName(); LOGGER.info("rule \"{}\" fired.", ruleName); } }); ((KnowledgeBaseImpl) rb).ruleBase.unlock(); return session; } private void populateGlobals(StatefulKnowledgeSession session) throws WorkflowException { Map<String, String> globals = rulemanager.listGlobals(); for (Map.Entry<String, String> global : globals.entrySet()) { Class<?> globalClass; try { globalClass = bundleContext.getBundle().loadClass(global.getValue()); } catch (ClassNotFoundException e) { throw new WorkflowException(String.format("Could not load class for global (%s)", global), e); } Filter filter = OsgiUtils.getFilterForLocation(globalClass, global.getKey(), ContextHolder.get().getCurrentContextId()); Object osgiServiceProxy = utilsService.getOsgiServiceProxy(filter, globalClass); session.setGlobal(global.getKey(), osgiServiceProxy); } } public void setBundleContext(BundleContext bundleContext) { this.bundleContext = bundleContext; utilsService = new DefaultOsgiUtilsService(bundleContext); } public void setRulemanager(DroolsRuleManager rulemanager) { this.rulemanager = rulemanager; } @Override public void cancelFlow(Long processInstanceId) throws WorkflowException { getSessionForCurrentContext().abortProcessInstance(processInstanceId); List<Task> tasksForProcessId = taskbox.getTasksForProcessId(Long.toString(processInstanceId)); for (Task t : tasksForProcessId) { taskbox.finishTask(t); } } public void setTaskbox(TaskboxService taskbox) { this.taskbox = taskbox; } public void setAuditingConnectors(Collection<AuditingDomain> auditingConnectors) { this.auditingConnectors = auditingConnectors; } public void setEventReceivers(Collection<EventSupport> eventReceivers) { this.eventReceivers = eventReceivers; } }