org.flowable.decision.DecisionAnalysisService.java Source code

Java tutorial

Introduction

Here is the source code for org.flowable.decision.DecisionAnalysisService.java

Source

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flowable.decision;

import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.spark.launcher.SparkAppHandle;
import org.apache.spark.launcher.SparkAppHandle.State;
import org.apache.spark.launcher.SparkLauncher;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ExclusiveGateway;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.ProcessEngine;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.form.model.OptionFormField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
 * @author Joram Barrez
 */
@Service
public class DecisionAnalysisService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DecisionAnalysisService.class);

    private RepositoryService repositoryService;

    @Autowired
    public DecisionAnalysisService(ProcessEngine processEngine) {
        this.repositoryService = processEngine.getRepositoryService();
    }

    @Scheduled(initialDelay = 1000, fixedDelay = 30000)
    public void analyze() {

        LOGGER.info("Starting decision analysis ...");

        // Hardcoded process definition for demo, but obviouysly can be parameterized
        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
                .processDefinitionKey("loan").latestVersion().singleResult();
        if (processDefinition == null) {
            LOGGER.info("No process definition found. No analysis done.");
            return;
        }
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());

        /*
            
        1) Look for pattern: user tasks with form + exclusive gateway + sequenceflows with form outcomes
            
        2) Calculate all paths from that user task to the start + gather the variables (form fields) along those paths
            
        3) Hand over to Spark ML
            
        */

        // Step (1)
        LOGGER.info("Analyzing process definition " + processDefinition.getId());
        List<UserTask> matchingUserTasks = findUserTasksMatchingPattern(bpmnModel);

        // For demo, only show 'Loan Review' task
        matchingUserTasks.removeIf(userTask -> "Loan Review".equals(userTask.getName()));

        // Step (2) 
        Map<String, List<String>> outcomesMap = new HashMap<>(); // userTask.id -> outcomes
        Map<String, Map<String, List<String>>> possibleValueCounts // userTask.id -> {variable, distinct_values_count}
                = determinePossibleVariablesForUserTasks(processDefinition, matchingUserTasks, outcomesMap);

        // Step (3)
        submitSparkAppsForTasks(processDefinition, outcomesMap, matchingUserTasks, possibleValueCounts);
    }

    private List<UserTask> findUserTasksMatchingPattern(BpmnModel bpmnModel) {
        List<UserTask> userTasks = bpmnModel.getMainProcess().findFlowElementsOfType(UserTask.class, true);
        List<UserTask> matchingUserTasks = userTasks.stream().filter(userTask -> {

            if (userTask.getFormKey() != null && userTask.getOutgoingFlows().size() == 1
                    && userTask.getOutgoingFlows().get(0).getTargetFlowElement() instanceof ExclusiveGateway) {

                ExclusiveGateway gw = (ExclusiveGateway) userTask.getOutgoingFlows().get(0).getTargetFlowElement();

                // All outgoing flows must either have an outcome or be the default flow
                for (SequenceFlow flow : gw.getOutgoingFlows()) {
                    if (!isOutcomeExpression(flow.getConditionExpression(), userTask.getFormKey())
                            && !flow.getId().equals(gw.getDefaultFlow())) {
                        return false;
                    }
                }
                ;

                return true;
            }
            return false;

        }).collect(Collectors.toList());
        return matchingUserTasks;
    }

    private Map<String, Map<String, List<String>>> determinePossibleVariablesForUserTasks(
            ProcessDefinition processDefinition, List<UserTask> matchingUserTasks,
            Map<String, List<String>> outcomesMap) {

        Map<String, Map<String, List<String>>> possibleValueCounts = new HashMap<>();
        for (UserTask matchingUserTask : matchingUserTasks) {

            LOGGER.info("Found matching pattern for user task " + matchingUserTask.getName()
                    + ". Collecting variables...");

            Map<String, List<String>> taskVariableValueCount = new HashMap<>();
            possibleValueCounts.put(matchingUserTask.getId(), taskVariableValueCount);

            //            FormDefinition formDefinition = formRepositoryService.createFormDefinitionQuery()
            //                    .formDefinitionKey(matchingUserTask.getFormKey())
            //                    .singleResult();
            //            FormInfo formInfo = formRepositoryService.getFormModelById(formDefinition.getId());
            //            FormModel formModel = formInfo.getFormModel();

            List<String> outcomes = new ArrayList<>();

            // Not yet supported on MongoDB
            outcomes.add("Reject");
            outcomes.add("Consider");
            outcomes.add("Accept");
            outcomesMap.put(matchingUserTask.getId(), outcomes);

            //            for (FormOutcome formOutcome : ((SimpleFormModel) formModel).getOutcomes()) {
            //                outcomes.add(formOutcome.getName());
            //            }

            taskVariableValueCount.put("fullname", new ArrayList<>());
            taskVariableValueCount.put("loan", new ArrayList<>());
            taskVariableValueCount.put("home", Arrays.asList("Rented", "Mortgaged", "Owned"));
            taskVariableValueCount.put("age", new ArrayList<>());
            taskVariableValueCount.put("nationality",
                    Arrays.asList("Belgian", "Dutch", "French", "German", "Italian", "Spanish", "UK", "other"));
            taskVariableValueCount.put("Income", new ArrayList<>());

            //            for (UserTask userTaskBeforeTask : findUserTasksBefore(matchingUserTask)) {
            //
            ////                formDefinition = formRepositoryService.createFormDefinitionQuery()
            ////                        .formDefinitionKey(userTaskBeforeTask.getFormKey())
            ////                        .singleResult();
            ////                formModel = formRepositoryService.getFormModelById(formDefinition.getId()).getFormModel();
            //
            //                for (FormField formField : ((SimpleFormModel) formModel).getFields()) {
            //
            //                    String variableName = formField.getId();
            //                    if (!taskVariableValueCount.containsKey(variableName)) {
            //                        taskVariableValueCount.put(variableName, new ArrayList<>());
            //                    }
            //                    List<String> variableValues = taskVariableValueCount.get(variableName);
            //
            //                    if (formField.getType().equals("radio-buttons") || formField.getType().equals("dropdown")) {
            //                        OptionFormField optionFormField = (OptionFormField) formField;
            //                        optionFormField.getOptions().forEach(option -> {
            //                            if (!variableValues.contains(option.getName()) && !isEmptyOption(optionFormField, option.getName())) {
            //                                variableValues.add(option.getName());
            //                            }
            //                        });
            //
            //                    }
            //                }
            //
            //            }

            LOGGER.info("Found following variable preceding user task " + matchingUserTask.getName() + ":");
            taskVariableValueCount.keySet().forEach(var -> LOGGER.info(var));
        }
        return possibleValueCounts;
    }

    private void submitSparkAppsForTasks(ProcessDefinition processDefinition, Map<String, List<String>> outcomesMap,
            List<UserTask> matchingUserTasks, Map<String, Map<String, List<String>>> possibleValueCounts) {
        for (UserTask matchingUserTask : matchingUserTasks) {
            LOGGER.info("Submitting Spark ML app for task " + matchingUserTask.getId() + "...");
            try {

                // Not so pretty: generating a long argument string to pass info to spark job. Should be handled with a persistent store really.

                /*
                 * Format (separated by # character):
                 * 
                 * - processDefinitionId
                 * - taskKey
                 * - outcome variable
                 * - outcome variable possibilities
                 * - variable names
                 * - variable possibilities
                 */

                StringBuilder argumentBuilder = new StringBuilder();
                argumentBuilder.append(processDefinition.getId()).append("#") // process definition id
                        .append(matchingUserTask.getId()).append("#") // task key
                        .append("form_" + matchingUserTask.getFormKey() + "_outcome").append("#"); // outcome variable

                List<String> outcomes = outcomesMap.get(matchingUserTask.getId());
                for (int i = 0; i < outcomes.size(); i++) {
                    argumentBuilder.append(outcomes.get(i)); // outcome variable output possibilities
                    if (i != outcomes.size() - 1) {
                        argumentBuilder.append(";");
                    }
                }
                argumentBuilder.append("#");

                Map<String, List<String>> variableToPotentialValues = possibleValueCounts
                        .get(matchingUserTask.getId());
                List<String> variableNames = new ArrayList<>(variableToPotentialValues.keySet());
                for (int i = 0; i < variableNames.size(); i++) {
                    argumentBuilder.append(variableNames.get(i)); // variable names
                    if (i != variableNames.size() - 1) {
                        argumentBuilder.append(";");
                    }
                }
                argumentBuilder.append("#");
                for (int i = 0; i < variableNames.size(); i++) {
                    List<String> possibleValues = variableToPotentialValues.get(variableNames.get(i));
                    for (int j = 0; j < possibleValues.size(); j++) {
                        argumentBuilder.append(possibleValues.get(j)); // variable possibilities
                        if (j != possibleValues.size() - 1) {
                            argumentBuilder.append("&");
                        }
                    }
                    if (i != variableNames.size() - 1) {
                        argumentBuilder.append(";");
                    }
                }

                LOGGER.info("Arguments for Spark app: " + argumentBuilder.toString());

                SparkAppHandle sparkAppHandle = new SparkLauncher().setSparkHome(System.getProperty("sparkHome"))
                        .setAppResource(System.getProperty("appResource"))
                        .setMainClass("org.flowable.AnalyseDecisions").setMaster("local[4]")
                        //                        .setVerbose(true)
                        .addAppArgs(argumentBuilder.toString()).redirectOutput(Redirect.INHERIT)
                        .startApplication(new SparkAppHandle.Listener() {

                            @Override
                            public void stateChanged(SparkAppHandle handle) {
                                LOGGER.info(handle.getState() + " new  state");
                            }

                            @Override
                            public void infoChanged(SparkAppHandle handle) {
                                LOGGER.info(handle.getState() + " new  state");
                            }
                        });

                // For demo: make sure the tasks are processed sequentially to not have the console output mixed for all tasks 
                while (!sparkAppHandle.getState().equals(State.FINISHED)
                        && !sparkAppHandle.getState().equals(State.FAILED)) {
                    Thread.sleep(5000L);
                }

            } catch (IOException e) {
                LOGGER.error("Could not submit app to Spark", e);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    private boolean isOutcomeExpression(String expression, String formKey) {
        if (StringUtils.isNotBlank(expression) && expression.startsWith("${form_" + formKey + "_outcome")
                && expression.endsWith("}")) {
            return true;
        }
        return false;
    }

    private boolean isEmptyOption(OptionFormField optionFormField, String value) {
        if (optionFormField.getHasEmptyValue() != null && optionFormField.getHasEmptyValue()) {
            Object defaultValue = optionFormField.getValue();
            if (defaultValue != null) {
                return defaultValue.equals(value);
            }
        }
        return false;
    }

    private List<UserTask> findUserTasksBefore(UserTask startUserTask) {
        List<UserTask> result = new ArrayList<>();

        Set<String> visitedFlowElements = new HashSet<>();
        LinkedList<FlowElement> elementsToVisit = new LinkedList<>();
        for (SequenceFlow flow : startUserTask.getIncomingFlows()) {
            elementsToVisit.add(flow.getSourceFlowElement());
        }

        while (!elementsToVisit.isEmpty()) {
            FlowElement flowElement = elementsToVisit.removeFirst();
            if (!visitedFlowElements.contains(flowElement.getId())) {

                if (flowElement instanceof FlowNode) {

                    FlowNode flowNode = (FlowNode) flowElement;
                    if (!flowNode.getIncomingFlows().isEmpty()) {
                        for (SequenceFlow flow : flowNode.getIncomingFlows()) {
                            elementsToVisit.add(flow.getSourceFlowElement());
                        }
                    }

                    if (flowElement instanceof UserTask) {
                        UserTask userTask = (UserTask) flowElement;
                        if (userTask.getFormKey() != null) {
                            result.add(userTask);
                        }
                    }

                }
            }
            visitedFlowElements.add(flowElement.getId());
        }
        return result;
    }

}