001/*
002 *  Copyright 2023 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workflow.dao;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030
031import org.ametys.core.ui.Callable;
032import org.ametys.core.util.I18nUtils;
033import org.ametys.plugins.workflow.component.WorkflowLanguageManager;
034import org.ametys.plugins.workflow.support.I18nHelper;
035import org.ametys.plugins.workflow.support.WorflowRightHelper;
036import org.ametys.plugins.workflow.support.WorkflowHelper;
037import org.ametys.plugins.workflow.support.WorkflowSessionHelper;
038import org.ametys.runtime.i18n.I18nizableText;
039import org.ametys.runtime.plugin.component.AbstractLogEnabled;
040
041import com.opensymphony.workflow.loader.AbstractDescriptor;
042import com.opensymphony.workflow.loader.ActionDescriptor;
043import com.opensymphony.workflow.loader.ConditionalResultDescriptor;
044import com.opensymphony.workflow.loader.ConditionsDescriptor;
045import com.opensymphony.workflow.loader.DescriptorFactory;
046import com.opensymphony.workflow.loader.ResultDescriptor;
047import com.opensymphony.workflow.loader.StepDescriptor;
048import com.opensymphony.workflow.loader.WorkflowDescriptor;
049
050/**
051 * DAO for workflow steps 
052 */
053public class WorkflowStepDAO extends AbstractLogEnabled implements Component, Serviceable
054{
055    /** The component's role */
056    public static final String ROLE =  WorkflowStepDAO.class.getName();
057    
058    /** Id for initial step */
059    public static final String INITIAL_STEP_ID = "step0";
060    
061    /** The default label for steps */
062    public static final I18nizableText DEFAULT_STEP_NAME = new I18nizableText("plugin.workflow", "PLUGIN_WORKFLOW_DEFAULT_STEP_LABEL");
063    
064    /** Default path for svg step icons */
065    private static final String __DEFAULT_SVG_STEP_ICON_PATH = "plugin:cms://resources/img/history/workflow/step_0_16.png";
066    
067    /** Default path for node step icons */
068    private static final String __DEFAULT_STEP_ICON_PATH = "/plugins/cms/resources/img/history/workflow/step_0_16.png";
069    
070    /** The workflow helper */
071    protected WorkflowHelper _workflowHelper;
072    
073    /** The helper for i18n translations and catalogs */
074    protected I18nHelper _i18nHelper;
075    
076    /** The workflow session helper */
077    protected WorkflowSessionHelper _workflowSessionHelper;
078    
079    /** The workflow right helper */
080    protected WorflowRightHelper _workflowRightHelper;
081    
082    /** The workflow condition DAO */
083    protected WorkflowConditionDAO _workflowConditionDAO;
084    
085    /** The workflow result DAO */
086    protected WorkflowResultDAO _workflowResultDAO;
087   
088    /** The workflow transition DAO */
089    protected WorkflowTransitionDAO _workflowTransitionDAO;
090    
091    /** The workflow language manager */
092    protected WorkflowLanguageManager _workflowLanguageManager;
093    
094    /** I18n Utils */
095    protected I18nUtils _i18nUtils;
096
097    public void service(ServiceManager smanager) throws ServiceException
098    {
099        _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE);
100        _workflowSessionHelper = (WorkflowSessionHelper) smanager.lookup(WorkflowSessionHelper.ROLE);
101        _workflowRightHelper = (WorflowRightHelper) smanager.lookup(WorflowRightHelper.ROLE);
102        _workflowConditionDAO = (WorkflowConditionDAO) smanager.lookup(WorkflowConditionDAO.ROLE);
103        _workflowTransitionDAO = (WorkflowTransitionDAO) smanager.lookup(WorkflowTransitionDAO.ROLE);
104        _workflowLanguageManager = (WorkflowLanguageManager) smanager.lookup(WorkflowLanguageManager.ROLE);
105        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
106        _i18nHelper = (I18nHelper) smanager.lookup(I18nHelper.ROLE);
107        _workflowResultDAO = (WorkflowResultDAO) smanager.lookup(WorkflowResultDAO.ROLE);
108    }
109    
110    /**
111     * Verify that current workflow has steps
112     * @param workflowName the workflow's unique name
113     * @return true if worflow has steps
114     */
115    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
116    public boolean hasSteps(String workflowName)
117    {
118        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
119        _workflowRightHelper.checkReadRight(workflowDescriptor);
120        return !workflowDescriptor.getSteps().isEmpty();
121    }
122    
123    /**
124     * Get the step editable infos 
125     * @param workflowName current workflow's id
126     * @param stepId current step's id
127     * @return a map of step infos and non-available ids
128     */
129    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
130    public Map<String, Object> getStepInfos(String workflowName, Integer stepId)
131    {
132        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
133        _workflowRightHelper.checkReadRight(workflowDescriptor);
134        Map<String, Object> stepInfos = new HashMap<>();
135        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor);
136        Map<String, String> translations = new HashMap<>();
137        if (stepId == null) //creation mode
138        {
139            int id = _getUniqueStepId(workflowDescriptor);
140            stepInfos.put("id", id);
141            translations.put(_workflowLanguageManager.getCurrentLanguage(), _i18nUtils.translate(DEFAULT_STEP_NAME));
142        }
143        else //edit mode
144        {
145            stepIds.remove(stepId);
146            stepInfos.put("id", stepId);
147            I18nizableText labelKey = getStepLabel(workflowDescriptor, stepId);
148            translations = _workflowSessionHelper.getTranslation(workflowName, labelKey);
149            if (translations == null) 
150            {
151                translations = Map.of(_workflowLanguageManager.getCurrentLanguage(), getStepLabelAsString(workflowDescriptor, stepId, false));
152            }
153        }
154        stepInfos.put("labels", translations);
155        stepInfos.put("ids", stepIds);
156        
157        return stepInfos;
158    }
159
160    @SuppressWarnings("unchecked")
161    private List<Integer> _getUsedStepIds(WorkflowDescriptor workflowDescriptor)
162    {
163        List<Integer> usedIds = (List<Integer>) workflowDescriptor.getSteps().stream()
164                .map(s -> ((StepDescriptor) s).getId())
165                .collect(Collectors.toList());
166        usedIds.add(0);
167        return usedIds;
168    }
169    
170    /**
171     * Create a new step and add it to current workflow
172     * @param workflowName current workflow's id
173     * @param stepId the new step id
174     * @param labels the new step labels
175     * @return  map of the step infos
176     */
177    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
178    public Map<String, Object> createStep(String workflowName, Integer stepId, Map<String, String> labels)
179    {
180        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
181        _workflowRightHelper.checkEditRight(workflowDescriptor);
182        Map<String, Object> results = new HashMap<>();
183        
184        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor); //test if new id is unique between workflow's steps
185        if (stepIds.contains(stepId) || stepId == 0)
186        {
187            results.put("message", "duplicate-id");
188            return results;
189        }
190        
191        DescriptorFactory factory = new DescriptorFactory();
192        StepDescriptor stepDescriptor = factory.createStepDescriptor();
193        stepDescriptor.setId(stepId);
194        I18nizableText stepLabelKey = _i18nHelper.generateI18nKey(workflowName, "STEP", stepId);
195        stepDescriptor.setName(stepLabelKey.toString());
196        workflowDescriptor.addStep(stepDescriptor);
197        
198        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
199        _workflowSessionHelper.updateTranslations(workflowName, stepLabelKey, labels);
200        
201        results.put("stepId", stepId);
202        results.put("stepLabels", labels);
203        results.put("workflowId", workflowName);
204        
205        return results;
206    }
207
208    private int _getUniqueStepId(WorkflowDescriptor workflowDescriptor)
209    {
210        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor);
211        int id = 1;
212        while (stepIds.contains(id))
213        {
214            id++;
215        }
216        return id;
217    }
218    
219    /**
220     * Edit the step label
221     * @param workflowName current workflow's id
222     * @param stepId the step's id
223     * @param newMainLabel the new label in the current application's language
224     * @return map of the step infos if edit worked, contain error message else
225     */
226    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
227    public Map<String, Object> editStepLabel(String workflowName, Integer stepId, String newMainLabel)
228    {
229        return editStep(workflowName, stepId, stepId, Map.of(_workflowLanguageManager.getCurrentLanguage(), newMainLabel)); 
230    }
231    
232    /**
233     * Edit the step
234     * @param workflowName current workflow's id
235     * @param oldId the step's last id
236     * @param id the step's new id 
237     * @param labels the new step labels
238     * @return map of the step infos if edit worked, contain error message else
239     */
240    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
241    public Map<String, Object> editStep(String workflowName, Integer oldId, Integer id, Map<String, String> labels)
242    {
243        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
244        _workflowRightHelper.checkEditRight(workflowDescriptor);
245        Map<String, Object> results = new HashMap<>();
246        
247        StepDescriptor stepDescriptor = workflowDescriptor.getStep(oldId);
248        
249        if (id != oldId) //if step id has been edited
250        {
251            if (!getIncomingActions(oldId, workflowDescriptor).isEmpty()) //edition on id can't happen if there are transitions leading to this step
252            {
253                results.put("message", "incoming-actions");
254                return results;
255            }
256            List<Integer> stepIds = _getUsedStepIds(workflowDescriptor); //test if new id is unique between workflow's steps
257            if (stepIds.contains(id))
258            {
259                results.put("message", "duplicate-id");
260                return results;
261            }
262            stepDescriptor.setId(id);
263        }
264        
265        String defaultCatalog = _workflowHelper.getWorkflowCatalog(workflowName);
266        I18nizableText labelKey = getStepLabel(workflowDescriptor, id);
267        if (!defaultCatalog.equals(labelKey.getCatalogue()))
268        {
269            labelKey = new I18nizableText(defaultCatalog, labelKey.getKey());
270            String newName = labelKey.toString();
271            stepDescriptor.setName(newName);
272        }
273        _workflowSessionHelper.updateTranslations(workflowName, labelKey, labels);
274        
275        results.put("stepId", id);
276        results.put("oldStepId", oldId);
277        results.put("stepLabels", labels);
278        results.put("workflowId", workflowName);
279        
280        return results;
281    }
282    
283    /**
284     * Delete the step from workflow
285     * @param workflowName current workflow's id
286     * @param stepId current step's id
287     * @return an error message if deleting couldn't proceed
288     */
289    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
290    public Map<String, Object> deleteStep(String workflowName, Integer stepId)
291    {
292        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
293        _workflowRightHelper.checkEditRight(workflowDescriptor);
294        Map<String, Object> results = new HashMap<>();
295        
296        StepDescriptor stepDescriptor = workflowDescriptor.getStep(stepId);
297        
298        if (getIncomingActions(stepId, workflowDescriptor).isEmpty()) //we can't delete this step if there are transitions having current step as result
299        {
300            I18nizableText stepLabel = getStepLabel(workflowDescriptor, stepId);
301            workflowDescriptor.getSteps().remove(stepDescriptor);
302            _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
303            _workflowSessionHelper.removeTranslation(workflowName, getStepLabel(stepDescriptor)); 
304            
305            results.put("stepId", stepId);
306            results.put("stepLabels", stepLabel);
307            results.put("workflowId", workflowName);
308        }
309        else
310        {
311            results.put("message", "incoming-actions");
312        }
313        
314        return results;
315    }
316
317    /**
318     * Get the step label as new I18nizableText
319     * @param stepDescriptor the current step
320     * @return the step label
321     */
322    public I18nizableText getStepLabel(StepDescriptor stepDescriptor)
323    {
324        return new I18nizableText("application", stepDescriptor.getName());
325    }
326    
327    
328    /**
329     * Get the workflow editor tree's nodes
330     * @param currentNode id of the current node
331     * @param workflowName unique name of current workflow
332     * @return a map of the current node's children
333     */
334    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
335    public Map<String, Object> getStepNodes(String currentNode, String workflowName) 
336    {
337        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
338        _workflowRightHelper.checkReadRight(workflowDescriptor);
339        List<Map<String, Object>> nodes = new ArrayList<>();
340        boolean canWrite = _workflowRightHelper.canWrite(workflowDescriptor);
341        
342        if (workflowName != null)
343        {
344            List<ActionDescriptor> initialActions = workflowDescriptor.getInitialActions();
345            if (currentNode.equals("root"))
346            {
347                //initial step
348                Map<String, Object> infosInitialStep = _step2JSON(workflowDescriptor, 0, initialActions.size() > 0, false, false);
349                nodes.add(infosInitialStep);
350                
351                //other steps
352                @SuppressWarnings("cast")
353                List<StepDescriptor> steps = (List<StepDescriptor>) workflowDescriptor.getSteps();
354                for (StepDescriptor step : steps)
355                {
356                    Map<String, Object> infos = _step2JSON(workflowDescriptor, step.getId(), step.getActions().size() > 0, false, canWrite);
357                    nodes.add(infos);
358                }
359            }
360            else if (currentNode.equals(INITIAL_STEP_ID))
361            {
362                //initial actions
363                for (ActionDescriptor initialAction : initialActions)
364                {
365                    nodes.add(_action2JSON(currentNode, initialAction, workflowDescriptor, canWrite));
366                }
367            }
368            else
369            {
370                //regular actions
371                StepDescriptor step = workflowDescriptor.getStep(Integer.valueOf(currentNode.substring("step".length())));
372                for (ActionDescriptor transition : (List<ActionDescriptor>) step.getActions())
373                {
374                    nodes.add(_action2JSON(currentNode, transition, workflowDescriptor, canWrite));
375                }
376            }
377        }
378        
379        return Map.of("steps", nodes);
380    }
381    
382    /**
383     * Get the action infos for tree panel node
384     * @param stepId id of current step  node
385     * @param action currently processed action
386     * @param workflowDescriptor current workflow
387     * @param canWrite true if current user has edition right on current workflow
388     * @return map of the action infos
389     */
390    protected Map<String, Object> _action2JSON(String stepId, ActionDescriptor action, WorkflowDescriptor workflowDescriptor, boolean canWrite)
391    {
392        Set<StepWithIcon> finalSteps = _getActionFinalSteps(action, workflowDescriptor);
393        Map<Integer, Object> finalStepNames = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.label()));
394        Map<Integer, Object> finalStepIcons = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.iconPath()));
395        
396        String iconPath = _workflowTransitionDAO.getActionIconPath(workflowDescriptor.getName(), action);
397        
398        Map<String, Object> infos = new HashMap<>();
399        
400        infos.put("id", stepId + "-action" + action.getId());
401        infos.put("elementId", action.getId());
402        infos.put("smallIcon", iconPath);
403        infos.put("label", _workflowTransitionDAO.getActionLabel(workflowDescriptor.getName(), action));
404        infos.put("elementType", "action");
405        infos.put("hasChildren", false);
406        infos.put("targetedStepNames", finalStepNames);
407        infos.put("targetedStepIcons", finalStepIcons);
408        infos.put("canWrite", canWrite);
409        
410        return infos;
411    }
412    
413    /**
414     * Get the conditional and unconditional results of current action 
415     * @param action the current action
416     * @param workflowDescriptor the current workflow
417     * @return a list of the final steps as (stepId, stepLabel, StepIconPath)
418     */
419    protected Set<StepWithIcon> _getActionFinalSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor) 
420    {
421        Set<StepWithIcon> steps = new HashSet<>();
422        for (StepDescriptor step : getOutgoingSteps(action, workflowDescriptor))
423        {
424            int stepId = step.getId();
425            steps.add(new StepWithIcon(
426                stepId, 
427                getStepLabelAsString(workflowDescriptor, stepId, false), 
428                getStepIconPath(workflowDescriptor, stepId))
429            );
430        }
431        
432        return steps;
433    }
434    
435    /**
436     * Get possible outgoing steps for action
437     * @param action the current action
438     * @param workflowDescriptor the current workflow
439     * @return a set of the outgoing steps
440     */
441    public Set<StepDescriptor> getOutgoingSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor)
442    {
443        Set<StepDescriptor> outgoingSteps = new HashSet<>();
444        ResultDescriptor unconditionalResult = action.getUnconditionalResult();
445        boolean hasSameStepTarget = false;
446        if (unconditionalResult.getStep() != -1)
447        {
448            StepDescriptor unconditionalStep = workflowDescriptor.getStep(unconditionalResult.getStep());
449            outgoingSteps.add(unconditionalStep);
450        }
451        else
452        {
453            hasSameStepTarget = true;
454        }
455        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
456        for (ConditionalResultDescriptor result : conditionalResults)
457        {
458            StepDescriptor conditionalStep = workflowDescriptor.getStep(result.getStep());
459            if (conditionalStep != null)
460            {
461                outgoingSteps.add(conditionalStep);
462            }
463            else
464            {
465                hasSameStepTarget = true;
466            }
467        }
468        if (hasSameStepTarget)
469        {
470            outgoingSteps.addAll(getIncomingSteps(action.getId(), workflowDescriptor));
471        }
472        
473        return outgoingSteps;
474    }
475    
476    /**
477     * Get possible incoming steps for action
478     * @param actionId the current action's id
479     * @param workflowDescriptor the current  workflow
480     * @return a set of the action's incoming steps
481     */
482    public Set<StepDescriptor> getIncomingSteps(int actionId, WorkflowDescriptor workflowDescriptor)
483    {
484        Set<StepDescriptor> incomingSteps = new HashSet<>();
485        List<StepDescriptor> steps = workflowDescriptor.getSteps();
486        for (StepDescriptor step : steps)
487        {
488            if (step.getAction(actionId) != null)
489            {
490                incomingSteps.add(step);
491            }
492        }
493        return incomingSteps;
494    }
495    
496    /**
497     * Get current action's final steps and associated conditions
498     * @param currentNode id of current node
499     * @param workflowName unique name of current workflow
500     * @param actionId id of current action
501     * @return a map of current node's children
502     */
503    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
504    public Map<String, Object> getFinalSteps(String currentNode, String workflowName, Integer actionId) 
505    {
506        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
507        List<Map<String, Object>> nodes = new ArrayList<>();
508        if (_workflowRightHelper.canRead(workflowDescriptor))
509        {
510            ActionDescriptor action = workflowDescriptor.getAction(actionId);
511            boolean canWrite = _workflowRightHelper.canWrite(workflowDescriptor);
512            
513            List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
514            
515            if (currentNode.equals("root")) //get results(steps) nodes
516            {
517                ResultDescriptor unconditionalResult = action.getUnconditionalResult();
518                int stepId = unconditionalResult.getStep();
519                Map<String, Object> step2json = _step2JSON(workflowDescriptor, stepId, false, stepId != -1, canWrite);
520                step2json.put("isConditional", false);
521                nodes.add(step2json);
522                
523                for (ConditionalResultDescriptor result : conditionalResults)
524                {
525                    stepId = result.getStep();
526                    Map<String, Object> conditionalStep2Json = _step2JSON(workflowDescriptor, stepId, !result.getConditions().isEmpty(), stepId != -1, canWrite);
527                    conditionalStep2Json.put("isConditional", true);
528                    nodes.add(conditionalStep2Json);
529                }
530            }
531            else //get conditions nodes,
532            {
533                //conditions to display
534                List<AbstractDescriptor> conditions = _workflowResultDAO.getChildrenResultConditions(currentNode, action, conditionalResults);
535                if (!conditions.isEmpty())
536                {
537                    String[] path = _workflowResultDAO.getPath(currentNode);
538                    int stepId = Integer.valueOf(path[0].substring(4));
539                    ConditionsDescriptor rootOperator = _workflowResultDAO.getRootResultConditions(conditionalResults, stepId).get(0);
540                    boolean rootIsAND = !rootOperator.getType().equals(WorkflowConditionDAO.OR);
541                    for (int i = 0; i < conditions.size(); i++)
542                    {
543                        nodes.add(_workflowConditionDAO.conditionToJSON(conditions.get(i), currentNode, i, rootIsAND));
544                    }
545                }
546            }
547        }
548        
549        return Map.of("results", nodes);
550    }
551    
552    /**
553     * Get step infos
554     * @param workflowDescriptor current workflow
555     * @param stepId id of current step
556     * @param hasChildren true if step has actions 
557     * @param showId true if id needs to be displayed in the label
558     * @param canWrite true if current user has edition right on current workflow
559     * @return a map of the step infos
560     */
561    protected Map<String, Object> _step2JSON(WorkflowDescriptor workflowDescriptor, int stepId, boolean hasChildren, boolean showId, boolean canWrite)
562    {
563        Map<String, Object> infos = new HashMap<>();
564        
565        infos.put("id", "step" + stepId);
566        infos.put("elementId", stepId);
567        infos.put("label", getStepLabelAsString(workflowDescriptor, stepId, showId));
568        infos.put("elementType", "step");
569        infos.put("hasChildren", hasChildren);
570        infos.put("canWrite", canWrite);
571        try
572        {
573            infos.put("smallIcon", getStepIconPath(workflowDescriptor, stepId));
574        }
575        catch (Exception e)
576        {
577            getLogger().error("An error occurred while getting icon path for step id {}", stepId, e);
578        }
579       
580        return infos;
581    }
582    
583    /**
584     * Get the workflow's steps available as unconditional result for actions
585     * @param workflowName the current workflow name
586     * @param actionId id of current action if exist, can be null
587     * @param isInitialState true if current selected state is the initial state
588     * @return a map of the workflow steps 
589     */
590    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
591    public Map<String, Object> getStatesToJson(String workflowName, Integer actionId, Boolean isInitialState)
592    {
593        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
594        _workflowRightHelper.checkReadRight(workflowDescriptor);
595        List<Map<String, Object>> states = new ArrayList<>();
596        List<Integer> conditionalStepIds = new ArrayList<>();
597        List<StepDescriptor> steps = workflowDescriptor.getSteps();
598        if (actionId != null)
599        {
600            ActionDescriptor action = workflowDescriptor.getAction(actionId);
601            List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
602            for (ConditionalResultDescriptor conditionalResult : conditionalResults)
603            {
604                conditionalStepIds.add(conditionalResult.getStep());
605            }
606        }
607        for (StepDescriptor step : steps)
608        {
609            int stepId = step.getId();
610            if (!conditionalStepIds.contains(stepId))
611            {
612                Map<String, Object> stateInfos = new HashMap<>();
613                stateInfos.put("id", stepId);
614                stateInfos.put("label", getStepLabelAsString(workflowDescriptor, stepId, true));
615                states.add(stateInfos);
616            }
617        }
618        if (!isInitialState && !conditionalStepIds.contains(-1))
619        {
620            //Same state
621            Map<String, Object> stateInfos = new HashMap<>();
622            stateInfos.put("id", -1);
623            stateInfos.put("label", new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_RESULTS_SAME_STEP"));
624            states.add(stateInfos);
625        }
626        
627        return Map.of("data", states);
628    }
629    
630    
631    /**
632     * Get the translated step label
633     * @param workflowDescriptor current workflow
634     * @param stepId id of current step
635     * @param showId true if id needs to be displayed in the label
636     * @return the step label as string
637     */
638    public String getStepLabelAsString(WorkflowDescriptor workflowDescriptor, int stepId, boolean showId)
639    {
640        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
641        return showId 
642                ? _i18nHelper.translateKey(workflowDescriptor.getName(), label, DEFAULT_STEP_NAME) + " (" + stepId + ")"
643                : _i18nHelper.translateKey(workflowDescriptor.getName(), label, DEFAULT_STEP_NAME);
644    }
645    
646    /**
647     * Get the step's icon path 
648     * @param workflowDescriptor current worklfow
649     * @param stepId id of current step
650     * @return the icon path
651     */
652    public String getStepIconPath(WorkflowDescriptor workflowDescriptor, int stepId) 
653    {
654        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
655        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowDescriptor.getName(), label);
656        return _workflowHelper.getElementIconPath(label, __DEFAULT_STEP_ICON_PATH);
657    }
658    
659    /**
660     * Get the step's icon path as base 64 for svg links
661     * @param workflowDescriptor current worklfow
662     * @param stepId id of current step
663     * @return the icon path as base 64
664     */
665    public String getStepIconPathAsBase64(WorkflowDescriptor workflowDescriptor, int stepId) 
666    {
667        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
668        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowDescriptor.getName(), label); 
669        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_STEP_ICON_PATH);
670    }
671    
672    /**
673     * Get the step i18n label
674     * @param workflowDescriptor current workflow
675     * @param stepId id of current step
676     * @return the i18n step label
677     */
678    public I18nizableText getStepLabel(WorkflowDescriptor workflowDescriptor, int stepId)
679    {
680        switch (stepId)
681        {
682            case -1:
683                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_RESULTS_SAME_STEP");
684            case 0:
685                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_INITIAL_STEP_NAME");
686            default:
687                StepDescriptor step = workflowDescriptor.getStep(stepId);
688                return getStepLabel(step);
689        }
690    }
691    
692    /**
693     * Get a list of actions outgoing from current step
694     * @param stepId id of current step
695     * @param workflow current workflow
696     * @return the list of outgoing actions
697     */
698    public List<ActionDescriptor> getOutgoingActions(int stepId, WorkflowDescriptor workflow)
699    {
700        return stepId != 0 
701                ? workflow.getStep(stepId).getActions()
702                : workflow.getInitialActions();
703    }  
704    
705    /**
706     * Get a set of actions incoming to current step
707     * @param stepId id of current step
708     * @param workflow current workflow
709     * @return the set of outgoing actions
710     */
711    @SuppressWarnings("unchecked")
712    public Set<ActionDescriptor> getIncomingActions(int stepId , WorkflowDescriptor workflow)
713    {
714        Set<ActionDescriptor> incomingActions = new HashSet<>();
715        if (stepId != 0)
716        {
717            incomingActions.addAll(_getIncomingActionsFromList(stepId, workflow.getInitialActions()));
718            List<StepDescriptor> steps = workflow.getSteps();
719            for (StepDescriptor otherSteps : steps)
720            {
721                if (otherSteps.getId() != stepId)
722                {
723                    List<ActionDescriptor> actions = otherSteps.getActions();
724                    incomingActions.addAll(_getIncomingActionsFromList(stepId, actions));
725                }
726            }
727        }
728        return incomingActions;
729    }
730    
731    /**
732     * Get a set of incoming actions if present in actions list
733     * @param stepId id of current step
734     * @param actions list of other step's actions
735     * @return a list containing other step's outgoing actions that are incoming to current step
736     */
737    protected Set<ActionDescriptor> _getIncomingActionsFromList(int stepId, List<ActionDescriptor> actions)
738    {
739        Set<ActionDescriptor> incoming = new HashSet<>();
740        for (ActionDescriptor action : actions)
741        {
742            ResultDescriptor unconditionalResult = action.getUnconditionalResult();
743            if (unconditionalResult.getStep() == stepId)
744            {
745                incoming.add(action);
746            }
747            else
748            {
749                boolean leadToStep = false;
750                List<ResultDescriptor> conditionalResults = action.getConditionalResults();
751                int indexResult = 0;
752                while (!leadToStep && indexResult < conditionalResults.size())
753                {
754                    if (conditionalResults.get(indexResult).getStep() == stepId)
755                    {
756                        incoming.add(action);
757                        leadToStep = true;
758                    }
759                    indexResult++;
760                }
761            }
762        } 
763        return incoming;
764    }
765    
766    /**
767     * Get id of the first step having current action, INITIAL_STEP_ID if current action is an initial action
768     * @param stepId id of current step
769     * @param steps list of all the steps in current workflow
770     * @param actionId id of current action
771     * @return the id of the first found step having current action
772     */
773    public String getFirstParentStepId(int stepId, List<StepDescriptor> steps, Integer actionId)
774    {
775        String firstParentStepId = "";
776        int i = 0;
777        do 
778        {
779            StepDescriptor otherStep = steps.get(i);
780            if (otherStep.getId() != stepId && otherStep.getAction(actionId) != null)
781            {
782                firstParentStepId = String.valueOf(otherStep.getId());
783            }
784            i++;
785        } while (firstParentStepId.isEmpty() && i < steps.size());
786        return firstParentStepId.isBlank() ? "0" : firstParentStepId;
787    }
788    
789    private record StepWithIcon(Integer id, String label, String iconPath) { /* empty */ }
790}