/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* 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.wso2.carbon.bpmn.analytics.publisher;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.history.HistoricActivityInstanceQuery;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.persistence.entity.VariableInstance;
import org.activiti.engine.impl.pvm.PvmEvent;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.task.TaskDefinition;
import org.activiti.engine.parse.BpmnParseHandler;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.bpmn.analytics.publisher.config.BPSAnalyticsConfiguration;
import org.wso2.carbon.bpmn.analytics.publisher.handlers.ProcessKPIParseHandler;
import org.wso2.carbon.bpmn.analytics.publisher.handlers.ProcessParseHandler;
import org.wso2.carbon.bpmn.analytics.publisher.handlers.TaskParseHandler;
import org.wso2.carbon.bpmn.analytics.publisher.internal.BPMNAnalyticsHolder;
import org.wso2.carbon.bpmn.analytics.publisher.listeners.ProcessTerminationKPIListener;
import org.wso2.carbon.bpmn.analytics.publisher.listeners.ProcessTerminationListener;
import org.wso2.carbon.bpmn.analytics.publisher.listeners.TaskCompletionListener;
import org.wso2.carbon.bpmn.core.BPMNEngineService;
import org.wso2.carbon.databridge.agent.DataPublisher;
import org.wso2.carbon.databridge.agent.exception.DataEndpointAgentConfigurationException;
import org.wso2.carbon.databridge.agent.exception.DataEndpointAuthenticationException;
import org.wso2.carbon.databridge.agent.exception.DataEndpointConfigurationException;
import org.wso2.carbon.databridge.agent.exception.DataEndpointException;
import org.wso2.carbon.databridge.commons.exception.TransportException;
import org.wso2.carbon.databridge.commons.utils.DataBridgeCommonsUtils;
import org.wso2.carbon.registry.api.Registry;
import org.wso2.carbon.registry.api.RegistryException;
import org.wso2.carbon.registry.api.Resource;
import org.wso2.carbon.registry.core.service.RegistryService;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class BPMNDataPublisher {
private static final Log log = LogFactory.getLog(BPMNDataPublisher.class);
private static final char PROC_VAR_VALUE_AVAILABLE = '1';
private static final char PROC_VAR_VALUE_UNAVAILABLE = '0';
private DataPublisher dataPublisher;
public void publishProcessEvent(HistoricProcessInstance processInstance) {
long endTime = System.currentTimeMillis();
Object[] payload = new Object[]{
processInstance.getProcessDefinitionId(),
processInstance.getId(),
processInstance.getStartActivityId(),
processInstance.getStartUserId(),
processInstance.getStartTime().toString(),
new Date(endTime).toString(),
(endTime - processInstance.getStartTime().getTime()),
processInstance.getTenantId()
};
if (log.isDebugEnabled()) {
log.debug("Starting to Publish BPMN process instance event. Process Definition ID:" + processInstance
.getProcessDefinitionId() + ", Process Instance ID:" + processInstance.getId());
}
if (dataPublisher != null) {
dataPublisher.tryPublish(getProcessStreamId(), getMeta(), null, payload);
if (log.isDebugEnabled()) {
log.debug("Published BPMN process instance event. Process Definition ID:" + processInstance
.getProcessDefinitionId() + ", Process Instance ID:" + processInstance.getId());
}
} else {
log.error("Data publisher is not registered. Events will not be published.");
}
}
public void publishTaskEvent(DelegateTask delegateTask) {
long endTime = System.currentTimeMillis();
Object[] payload = new Object[]{
delegateTask.getTaskDefinitionKey(),
delegateTask.getId(),
delegateTask.getProcessDefinitionId(),
delegateTask.getProcessInstanceId(),
delegateTask.getCreateTime().toString(),
delegateTask.getCreateTime().toString(),
new Date(endTime).toString(),
(endTime - delegateTask.getCreateTime().getTime()),
delegateTask.getAssignee()
};
if (log.isDebugEnabled()) {
log.debug("Starting to Publish BPMN task instance event. Process Definition ID: " + delegateTask
.getProcessDefinitionId() + ", Process Instance ID:" + delegateTask.getProcessInstanceId() +
", Task ID:" + delegateTask.getId());
}
if (dataPublisher != null) {
dataPublisher.tryPublish(getTaskInstanceStreamId(), getMeta(), null, payload);
if (log.isDebugEnabled()) {
log.debug("Published BPMN task instance event. Process Definition ID: " + delegateTask
.getProcessDefinitionId() + ", Process Instance ID:" + delegateTask.getProcessInstanceId() +
", Task ID:" + delegateTask.getId());
}
} else {
log.error("Data publisher is not registered. Events will not be published.");
}
}
/**
* @param activityInstanceQuery
*/
public void publishServiceTaskEvent(HistoricActivityInstanceQuery activityInstanceQuery) {
if (log.isDebugEnabled()) {
log.debug("Start to Publish BPMN service task instance event... ");
}
List<HistoricActivityInstance> historicActivityInstances = activityInstanceQuery.list();
for (HistoricActivityInstance instance : historicActivityInstances) {
if (instance.getActivityType().equals(AnalyticsPublisherConstants.SERVICE_TASK)) {
Object[] payload = new Object[]{
//Service task definition Id
instance.getActivityId(),
//task instance Id
instance.getId(),
//process definition id
instance.getProcessDefinitionId(),
//process instance Id
instance.getProcessInstanceId(),
//task created time
instance.getStartTime().toString(),
//task started time
instance.getStartTime().toString(),
//task end time
instance.getEndTime().toString(),
//task duration
instance.getDurationInMillis(),
//task assignee - NA as this is a service task
"NA"
};
if (dataPublisher != null) {
dataPublisher.tryPublish(getServiceTaskInstanceStreamId(), getMeta(), null, payload);
if (log.isDebugEnabled()) {
log.debug("Published BPMN service task instance event... Service task definition Id:" + instance
.getActivityId() + ", task instance Id:" + instance.getId() + ", process definition "
+ "id:" + instance.getProcessDefinitionId() + ", process instance Id:" + instance
.getProcessInstanceId());
}
} else {
log.error("Data publisher is not registered. Events will not be published.");
}
}
}
}
public void configure() throws IOException, XMLStreamException, DataEndpointAuthenticationException,
DataEndpointAgentConfigurationException, TransportException, DataEndpointException,
DataEndpointConfigurationException {
// Read bps analytics configuration
BPSAnalyticsConfiguration bpsAnalyticsConfiguration = BPMNAnalyticsHolder.getInstance().getBPSAnalyticsServer
().getBPSAnalyticsConfiguration();
if (bpsAnalyticsConfiguration.isBpmnDataPublishingEnabled() || bpsAnalyticsConfiguration
.isBpmnKPIDataPublishingEnabled()) {
configDataPublishing(bpsAnalyticsConfiguration.getAnalyticsReceiverURLSet(),
bpsAnalyticsConfiguration.getAnalyticsServerUsername(), bpsAnalyticsConfiguration
.getAnalyticsServerPassword(), bpsAnalyticsConfiguration.getAnalyticsAuthURLSet()
, bpsAnalyticsConfiguration.getBpmnAnalyticsPublisherType(), bpsAnalyticsConfiguration
.isBpmnAsyncDataPublishingEnabled(), bpsAnalyticsConfiguration
.isBpmnDataPublishingEnabled(), bpsAnalyticsConfiguration.isBpmnKPIDataPublishingEnabled());
}
}
/**
* Configure for data publishing to DAS for analytics
*
* @param receiverURLSet Analytics receiver's url
* @param username Analytics server's username
* @param password Analytics server's password
* @param authURLSet Analytics Auth URL set
* @param type Bpmn Analytics Publisher Type
* @param asyncDataPublishingEnabled is async Data Publishing Enabled
* @param genericAnalyticsEnabled is generic Analytics Enabled
* @param kpiAnalyticsEnabled is KPI Analytics Enabled
* @throws DataEndpointAuthenticationException
* @throws DataEndpointAgentConfigurationException
* @throws TransportException
* @throws DataEndpointException
* @throws DataEndpointConfigurationException
*/
void configDataPublishing(String receiverURLSet, String username, String password, String authURLSet, String type,
boolean asyncDataPublishingEnabled, boolean genericAnalyticsEnabled, boolean
kpiAnalyticsEnabled)
throws DataEndpointAuthenticationException, DataEndpointAgentConfigurationException, TransportException,
DataEndpointException, DataEndpointConfigurationException {
if (receiverURLSet != null && username != null && password != null) {
// Configure data publisher to be used by all data publishing listeners
if (log.isDebugEnabled()) {
log.debug("Creating BPMN analytics data publisher with Receiver URL: " +
receiverURLSet + ", Auth URL: " + authURLSet + " and Data publisher type: " + type);
}
dataPublisher = new DataPublisher(type, receiverURLSet, authURLSet, username, password);
BPMNAnalyticsHolder.getInstance().setAsyncDataPublishingEnabled(asyncDataPublishingEnabled);
BPMNEngineService engineService = BPMNAnalyticsHolder.getInstance().getBpmnEngineService();
// Attach data publishing listeners to all existing processes
if (log.isDebugEnabled()) {
log.debug("Attaching data publishing listeners to already deployed processes...");
}
RepositoryService repositoryService = engineService.getProcessEngine().getRepositoryService();
List<ProcessDefinition> processDefinitions = repositoryService.createProcessDefinitionQuery().list();
for (ProcessDefinition processDefinition : processDefinitions) {
// Process definition returned by the query does not contain all details such as task definitions.
// And it is also not the actual process definition, but a copy of it, so attaching listners to
// them is useless. Therefore, we have to fetch the complete process definition from the repository
// again.
ProcessDefinition completeProcessDefinition = repositoryService.
getProcessDefinition(processDefinition.getId());
if (completeProcessDefinition instanceof ProcessDefinitionEntity) {
ProcessDefinitionEntity processDefinitionEntity = (ProcessDefinitionEntity)
completeProcessDefinition;
if (genericAnalyticsEnabled) {
processDefinitionEntity
.addExecutionListener(PvmEvent.EVENTNAME_END, new ProcessTerminationListener());
}
if (kpiAnalyticsEnabled) {
processDefinitionEntity
.addExecutionListener(PvmEvent.EVENTNAME_END, new ProcessTerminationKPIListener());
}
Map<String, TaskDefinition> tasks = processDefinitionEntity.getTaskDefinitions();
List<ActivityImpl> activities = processDefinitionEntity.getActivities();
for (ActivityImpl activity : activities) {
if (activity.getProperty("type").toString().equalsIgnoreCase("usertask")) {
tasks.get(activity.getId())
.addTaskListener(TaskListener.EVENTNAME_COMPLETE, new TaskCompletionListener());
}
// We are publishing analytics data of service tasks in process termination ATM.
// else if(activity.getProperty("type").toString().equalsIgnoreCase("servicetask")){
// activity.addExecutionListener(PvmEvent.EVENTNAME_END,new
// ServiceTaskCompletionListener());
// }
}
}
}
// Configure parse handlers, which attaches data publishing listeners to new processes
if (log.isDebugEnabled()) {
log.debug("Associating parse handlers for processes and tasks, so that data publishing listeners "
+ "will be attached to new processes.");
}
ProcessEngineConfigurationImpl engineConfig = (ProcessEngineConfigurationImpl) engineService.
getProcessEngine().getProcessEngineConfiguration();
if (engineConfig.getPostBpmnParseHandlers() == null) {
engineConfig.setPostBpmnParseHandlers(new ArrayList<BpmnParseHandler>());
}
if (genericAnalyticsEnabled) {
engineConfig.getPostBpmnParseHandlers().add(new ProcessParseHandler());
engineConfig.getPostBpmnParseHandlers().add(new TaskParseHandler());
engineConfig.getBpmnDeployer().getBpmnParser().getBpmnParserHandlers().
addHandler(new ProcessParseHandler());
engineConfig.getBpmnDeployer().getBpmnParser().getBpmnParserHandlers().
addHandler(new TaskParseHandler());
}
if (kpiAnalyticsEnabled) {
engineConfig.getPostBpmnParseHandlers().add(new ProcessKPIParseHandler());
engineConfig.getBpmnDeployer().getBpmnParser().getBpmnParserHandlers()
.addHandler(new ProcessKPIParseHandler());
}
} else {
log.warn("Required fields for data publisher are not configured. Receiver URLs, username and password "
+ "are mandatory. Data publishing will not be enabled.");
}
}
/**
* Get meta data of the instances to publish them.
*
* @return new object array
*/
private Object[] getMeta() {
return new Object[] {};
}
/**
* Get StreamId for Task Instances.
*
* @return StreamId
*/
private String getTaskInstanceStreamId() {
return DataBridgeCommonsUtils.generateStreamId(AnalyticsPublisherConstants.TASK_STREAM_NAME,
AnalyticsPublisherConstants.STREAM_VERSION);
}
/**
* Get StreamId for Service Task instances
*
* @return StreamId
*/
private String getServiceTaskInstanceStreamId() {
return DataBridgeCommonsUtils.generateStreamId(AnalyticsPublisherConstants.SERVICE_TASK_STREAM_NAME,
AnalyticsPublisherConstants.STREAM_VERSION);
}
/**
* Get StreamId for processes.
*
* @return StreamId
*/
private String getProcessStreamId() {
return DataBridgeCommonsUtils.generateStreamId(AnalyticsPublisherConstants.PROCESS_STREAM_NAME,
AnalyticsPublisherConstants.STREAM_VERSION);
}
/**
* Publish the separate event to the DAS for the process variables for the called process instance
*
* @param processInstance process instance object
* @throws BPMNDataPublisherException
* @throws IOException
*/
public void publishKPIvariableData(ProcessInstance processInstance) throws BPMNDataPublisherException, IOException {
String processDefinitionId = processInstance.getProcessDefinitionId();
String processInstanceId = processInstance.getId();
String eventStreamId;
Object[] payload = new Object[0];
try {
JsonNode kpiConfig = getKPIConfiguration(processDefinitionId);
// do not publish the KPI event if DAS configurations are not done by the PC
if (kpiConfig == null) {
return;
}
JsonNode configedProcessVariables = kpiConfig.withArray(AnalyticsPublisherConstants
.PROCESS_VARIABLES_JSON_ENTRY_NAME);
if (log.isDebugEnabled()) {
log.debug(
"Publishing Process Variables (KPI) for the process instance " + processInstanceId + " of the "
+ "process : " + processDefinitionId);
}
/* Keeps configured process variable data as a JSON. These variables are sent as payload data to DAS.
Example value:
[{"name":"processInstanceId","type":"string","isAnalyzeData":"false","isDrillDownData":"false"},
{"name":"valuesAvailability","type":"string","isAnalyzeData":"false","isDrillDownData":"false"},
{"name":"custid","type":"string","isAnalyzeData":false,"isDrillDownData":false},
{"name":"amount","type":"long","isAnalyzeData":false,"isDrillDownData":false},
{"name":"confirm","type":"bool","isAnalyzeData":false,"isDrillDownData":false}]
*/
JsonNode fieldsConfigedForStreamPayload = kpiConfig
.withArray(AnalyticsPublisherConstants.PROCESS_VARIABLES_JSON_ENTRY_NAME);
eventStreamId = kpiConfig.get("eventStreamId").textValue();
Map<String, VariableInstance> variableInstances = ((ExecutionEntity) processInstance)
.getVariableInstances();
payload = new Object[fieldsConfigedForStreamPayload.size()];
//set process instance id as the 1st payload variable value
payload[0] = processInstanceId;
//availability of values for each process variable is represented by this char array as '1' (value available)
// or '0' (not available) in respective array index
char[] valueAvailabiliy = new char[fieldsConfigedForStreamPayload.size() - 2];
for (int i = 2; i < configedProcessVariables.size(); i++) {
String varName = (fieldsConfigedForStreamPayload.get(i)).get("name").textValue();
String varType = (fieldsConfigedForStreamPayload.get(i)).get("type").textValue();
Object varValue = variableInstances.get(varName).getValue();
switch (varType) {
case "int":
if (varValue == null) {
payload[i] = 0;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = Integer.parseInt(varValue.toString());
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
case "float":
if (varValue == null) {
payload[i] = 0;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = Float.parseFloat(varValue.toString());
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
case "long":
if (varValue == null) {
payload[i] = 0;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = Long.parseLong(varValue.toString());
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
case "double":
if (varValue == null) {
payload[i] = 0;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = Double.parseDouble(varValue.toString());
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
case "string":
if (varValue == null) {
payload[i] = "NA";
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = varValue;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
case "bool":
if (varValue == null) {
payload[i] = false;
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_UNAVAILABLE;
} else {
payload[i] = Boolean.parseBoolean(varValue.toString());
valueAvailabiliy[i - 2] = PROC_VAR_VALUE_AVAILABLE;
}
break;
default:
String errMsg = "Configured process variable type: \"" + varType + "\" of the variable \"" +
varName
+ "\" is not a WSO2 DAS applicable type for the process:" + processDefinitionId;
throw new BPMNDataPublisherException(errMsg);
}
}
//set meta data string value representing availability of values for each process variable
payload[1] = String.valueOf(valueAvailabiliy);
boolean dataPublishingSuccess = dataPublisher.tryPublish(eventStreamId, getMeta(), null, payload);
if (dataPublishingSuccess) {
if (log.isDebugEnabled()) {
log.debug("Published BPMN process instance KPI event... Process Instance Id :" + processInstanceId
+ ", Process Definition Id:" + processDefinitionId);
}
} else {
if (log.isDebugEnabled()) {
log.debug("Failed Publishing BPMN process instance KPI event... Process Instance Id :" +
processInstanceId + ", Process Definition Id:" + processDefinitionId);
}
}
} catch (RegistryException | RuntimeException | BPMNDataPublisherException e) {
String strMsg = "Failed Publishing BPMN process instance KPI event... Process Instance Id :" +
processInstanceId + ", Process Definition Id:" + processDefinitionId;
throw new BPMNDataPublisherException(strMsg, e);
}
}
/**
* Get DAS config details of given certain process which are configured for analytics from the config registry
*
* @param processDefinitionId Process definition ID
* @return KPI configuration details in JSON format. Ex:<p>
* {"processDefinitionId":"myProcess3:1:32518","eventStreamName":"t_666_process_stream","eventStreamVersion":"1.0.0"
* ,"eventStreamDescription":"This is the event stream generated to configure process analytics with DAS, for the
* processt_666","eventStreamNickName":"t_666_process_stream","eventStreamId":"t_666_process_stream:1.0.0",
* "eventReceiverName":"t_666_process_receiver","pcProcessId":"t:666",
* "processVariables":[{"name":"processInstanceId","type":"string","isAnalyzeData":"false",
* "isDrillDownData":"false"}
* ,{"name":"valuesAvailability","type":"string","isAnalyzeData":"false","isDrillDownData":"false"}
* ,{"name":"custid","type":"string","isAnalyzeData":false,"isDrillDownData":false}
* ,{"name":"amount","type":"long","isAnalyzeData":false,"isDrillDownData":false}
* ,{"name":"confirm","type":"bool","isAnalyzeData":false,"isDrillDownData":false}]}
* @throws RegistryException
*/
public JsonNode getKPIConfiguration(String processDefinitionId) throws RegistryException, IOException {
String resourcePath = AnalyticsPublisherConstants.REG_PATH_BPMN_ANALYTICS + processDefinitionId + "/"
+ AnalyticsPublisherConstants.ANALYTICS_CONFIG_FILE_NAME;
try {
RegistryService registryService = BPMNAnalyticsHolder.getInstance().getRegistryService();
Registry configRegistry = registryService.getConfigSystemRegistry();
if (configRegistry.resourceExists(resourcePath)) {
Resource processRegistryResource = configRegistry.get(resourcePath);
String dasConfigDetailsJSONStr = new String((byte[]) processRegistryResource.getContent(),
StandardCharsets.UTF_8);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(dasConfigDetailsJSONStr);
}
return null;
} catch (RegistryException e) {
String errMsg = "Error in Getting DAS config details of given process definition id :" + processDefinitionId
+ " from the BPS Config registry-" + resourcePath;
throw new RegistryException(errMsg, e);
}
}
}