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.CHECKED_BY_IMPLEMENTATION)
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.CHECKED_BY_IMPLEMENTATION)
130    public Map<String, Object> getStepInfos(String workflowName, Integer stepId)
131    {
132        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
133        
134        // Check user right
135        _workflowRightHelper.checkReadRight(workflowDescriptor);
136        
137        Map<String, Object> stepInfos = new HashMap<>();
138        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor);
139        Map<String, String> translations = new HashMap<>();
140        if (stepId == null) //creation mode
141        {
142            int id = _getUniqueStepId(workflowDescriptor);
143            stepInfos.put("id", id);
144            translations.put(_workflowLanguageManager.getCurrentLanguage(), _i18nUtils.translate(DEFAULT_STEP_NAME));
145        }
146        else //edit mode
147        {
148            stepIds.remove(stepId);
149            stepInfos.put("id", stepId);
150            I18nizableText labelKey = getStepLabel(workflowDescriptor, stepId);
151            translations = _workflowSessionHelper.getTranslation(workflowName, labelKey);
152            if (translations == null)
153            {
154                translations = Map.of(_workflowLanguageManager.getCurrentLanguage(), getStepLabelAsString(workflowDescriptor, stepId, false));
155            }
156        }
157        stepInfos.put("labels", translations);
158        stepInfos.put("ids", stepIds);
159        
160        return stepInfos;
161    }
162
163    @SuppressWarnings("unchecked")
164    private List<Integer> _getUsedStepIds(WorkflowDescriptor workflowDescriptor)
165    {
166        List<Integer> usedIds = (List<Integer>) workflowDescriptor.getSteps().stream()
167                .map(s -> ((StepDescriptor) s).getId())
168                .collect(Collectors.toList());
169        usedIds.add(0);
170        return usedIds;
171    }
172    
173    /**
174     * Create a new step and add it to current workflow
175     * @param workflowName current workflow's id
176     * @param stepId the new step id
177     * @param labels the new step labels
178     * @return  map of the step infos
179     */
180    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
181    public Map<String, Object> createStep(String workflowName, Integer stepId, Map<String, String> labels)
182    {
183        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
184        
185        // Check user right
186        _workflowRightHelper.checkEditRight(workflowDescriptor);
187        
188        Map<String, Object> results = new HashMap<>();
189        
190        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor); //test if new id is unique between workflow's steps
191        if (stepIds.contains(stepId) || stepId == 0)
192        {
193            results.put("message", "duplicate-id");
194            return results;
195        }
196        
197        DescriptorFactory factory = new DescriptorFactory();
198        StepDescriptor stepDescriptor = factory.createStepDescriptor();
199        stepDescriptor.setId(stepId);
200        I18nizableText stepLabelKey = _i18nHelper.generateI18nKey(workflowName, "STEP", stepId);
201        stepDescriptor.setName(stepLabelKey.toString());
202        workflowDescriptor.addStep(stepDescriptor);
203        
204        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
205        _workflowSessionHelper.updateTranslations(workflowName, stepLabelKey, labels);
206        
207        results.put("stepId", stepId);
208        results.put("stepLabels", labels);
209        results.put("workflowId", workflowName);
210        
211        return results;
212    }
213
214    private int _getUniqueStepId(WorkflowDescriptor workflowDescriptor)
215    {
216        List<Integer> stepIds = _getUsedStepIds(workflowDescriptor);
217        int id = 1;
218        while (stepIds.contains(id))
219        {
220            id++;
221        }
222        return id;
223    }
224    
225    /**
226     * Edit the step label
227     * @param workflowName current workflow's id
228     * @param stepId the step's id
229     * @param newMainLabel the new label in the current application's language
230     * @return map of the step infos if edit worked, contain error message else
231     */
232    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
233    public Map<String, Object> editStepLabel(String workflowName, Integer stepId, String newMainLabel)
234    {
235        return editStep(workflowName, stepId, stepId, Map.of(_workflowLanguageManager.getCurrentLanguage(), newMainLabel));
236    }
237    
238    /**
239     * Edit the step
240     * @param workflowName current workflow's id
241     * @param oldId the step's last id
242     * @param id the step's new id
243     * @param labels the new step labels
244     * @return map of the step infos if edit worked, contain error message else
245     */
246    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
247    public Map<String, Object> editStep(String workflowName, Integer oldId, Integer id, Map<String, String> labels)
248    {
249        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
250        
251        // Check user right
252        _workflowRightHelper.checkEditRight(workflowDescriptor);
253        
254        Map<String, Object> results = new HashMap<>();
255        
256        StepDescriptor stepDescriptor = workflowDescriptor.getStep(oldId);
257        
258        if (id != oldId) //if step id has been edited
259        {
260            if (!getIncomingActions(oldId, workflowDescriptor).isEmpty()) //edition on id can't happen if there are transitions leading to this step
261            {
262                results.put("message", "incoming-actions");
263                return results;
264            }
265            List<Integer> stepIds = _getUsedStepIds(workflowDescriptor); //test if new id is unique between workflow's steps
266            if (stepIds.contains(id))
267            {
268                results.put("message", "duplicate-id");
269                return results;
270            }
271            stepDescriptor.setId(id);
272        }
273        
274        String defaultCatalog = _workflowHelper.getWorkflowCatalog(workflowName);
275        I18nizableText labelKey = getStepLabel(workflowDescriptor, id);
276        if (!defaultCatalog.equals(labelKey.getCatalogue()))
277        {
278            labelKey = new I18nizableText(defaultCatalog, labelKey.getKey());
279            String newName = labelKey.toString();
280            stepDescriptor.setName(newName);
281        }
282        _workflowSessionHelper.updateTranslations(workflowName, labelKey, labels);
283        
284        results.put("stepId", id);
285        results.put("oldStepId", oldId);
286        results.put("stepLabels", labels);
287        results.put("workflowId", workflowName);
288        
289        return results;
290    }
291    
292    /**
293     * Delete the step from workflow
294     * @param workflowName current workflow's id
295     * @param stepId current step's id
296     * @return an error message if deleting couldn't proceed
297     */
298    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
299    public Map<String, Object> deleteStep(String workflowName, Integer stepId)
300    {
301        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
302        
303        // Check user right
304        _workflowRightHelper.checkEditRight(workflowDescriptor);
305        
306        Map<String, Object> results = new HashMap<>();
307        
308        StepDescriptor stepDescriptor = workflowDescriptor.getStep(stepId);
309        
310        if (getIncomingActions(stepId, workflowDescriptor).isEmpty()) //we can't delete this step if there are transitions having current step as result
311        {
312            I18nizableText stepLabel = getStepLabel(workflowDescriptor, stepId);
313            workflowDescriptor.getSteps().remove(stepDescriptor);
314            _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
315            _workflowSessionHelper.removeTranslation(workflowName, getStepLabel(stepDescriptor));
316            
317            results.put("stepId", stepId);
318            results.put("stepLabels", stepLabel);
319            results.put("workflowId", workflowName);
320        }
321        else
322        {
323            results.put("message", "incoming-actions");
324        }
325        
326        return results;
327    }
328
329    /**
330     * Get the step label as new I18nizableText
331     * @param stepDescriptor the current step
332     * @return the step label
333     */
334    public I18nizableText getStepLabel(StepDescriptor stepDescriptor)
335    {
336        return new I18nizableText("application", stepDescriptor.getName());
337    }
338    
339    
340    /**
341     * Get the workflow editor tree's nodes
342     * @param currentNode id of the current node
343     * @param workflowName unique name of current workflow
344     * @return a map of the current node's children
345     */
346    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
347    public Map<String, Object> getStepNodes(String currentNode, String workflowName)
348    {
349        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
350        
351        // Check user right
352        _workflowRightHelper.checkReadRight(workflowDescriptor);
353        
354        List<Map<String, Object>> nodes = new ArrayList<>();
355        boolean canWrite = _workflowRightHelper.canWrite(workflowDescriptor);
356        
357        if (workflowName != null)
358        {
359            List<ActionDescriptor> initialActions = workflowDescriptor.getInitialActions();
360            if (currentNode.equals("root"))
361            {
362                //initial step
363                Map<String, Object> infosInitialStep = _step2JSON(workflowDescriptor, 0, initialActions.size() > 0, false, false);
364                nodes.add(infosInitialStep);
365                
366                //other steps
367                List<StepDescriptor> steps = workflowDescriptor.getSteps();
368                for (StepDescriptor step : steps)
369                {
370                    Map<String, Object> infos = _step2JSON(workflowDescriptor, step.getId(), step.getActions().size() > 0, false, canWrite);
371                    nodes.add(infos);
372                }
373            }
374            else if (currentNode.equals(INITIAL_STEP_ID))
375            {
376                //initial actions
377                for (ActionDescriptor initialAction : initialActions)
378                {
379                    nodes.add(_action2JSON(currentNode, initialAction, workflowDescriptor, canWrite));
380                }
381            }
382            else
383            {
384                //regular actions
385                StepDescriptor step = workflowDescriptor.getStep(Integer.valueOf(currentNode.substring("step".length())));
386                for (ActionDescriptor transition : (List<ActionDescriptor>) step.getActions())
387                {
388                    nodes.add(_action2JSON(currentNode, transition, workflowDescriptor, canWrite));
389                }
390            }
391        }
392        
393        return Map.of("steps", nodes);
394    }
395    
396    /**
397     * Get the action infos for tree panel node
398     * @param stepId id of current step  node
399     * @param action currently processed action
400     * @param workflowDescriptor current workflow
401     * @param canWrite true if current user has edition right on current workflow
402     * @return map of the action infos
403     */
404    protected Map<String, Object> _action2JSON(String stepId, ActionDescriptor action, WorkflowDescriptor workflowDescriptor, boolean canWrite)
405    {
406        Set<StepWithIcon> finalSteps = _getActionFinalSteps(action, workflowDescriptor);
407        Map<Integer, Object> finalStepNames = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.label()));
408        Map<Integer, Object> finalStepIcons = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.iconPath()));
409        
410        String iconPath = _workflowTransitionDAO.getActionIconPath(workflowDescriptor.getName(), action);
411        
412        Map<String, Object> infos = new HashMap<>();
413        
414        infos.put("id", stepId + "-action" + action.getId());
415        infos.put("elementId", action.getId());
416        infos.put("smallIcon", iconPath);
417        infos.put("label", _workflowTransitionDAO.getActionLabel(workflowDescriptor.getName(), action));
418        infos.put("elementType", "action");
419        infos.put("hasChildren", false);
420        infos.put("targetedStepNames", finalStepNames);
421        infos.put("targetedStepIcons", finalStepIcons);
422        infos.put("canWrite", canWrite);
423        
424        return infos;
425    }
426    
427    /**
428     * Get the conditional and unconditional results of current action
429     * @param action the current action
430     * @param workflowDescriptor the current workflow
431     * @return a list of the final steps as (stepId, stepLabel, StepIconPath)
432     */
433    protected Set<StepWithIcon> _getActionFinalSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor)
434    {
435        Set<StepWithIcon> steps = new HashSet<>();
436        for (StepDescriptor step : getOutgoingSteps(action, workflowDescriptor))
437        {
438            int stepId = step.getId();
439            steps.add(new StepWithIcon(
440                stepId,
441                getStepLabelAsString(workflowDescriptor, stepId, false),
442                getStepIconPath(workflowDescriptor, stepId))
443            );
444        }
445        
446        return steps;
447    }
448    
449    /**
450     * Get possible outgoing steps for action
451     * @param action the current action
452     * @param workflowDescriptor the current workflow
453     * @return a set of the outgoing steps
454     */
455    public Set<StepDescriptor> getOutgoingSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor)
456    {
457        Set<StepDescriptor> outgoingSteps = new HashSet<>();
458        ResultDescriptor unconditionalResult = action.getUnconditionalResult();
459        boolean hasSameStepTarget = false;
460        if (unconditionalResult.getStep() != -1)
461        {
462            StepDescriptor unconditionalStep = workflowDescriptor.getStep(unconditionalResult.getStep());
463            outgoingSteps.add(unconditionalStep);
464        }
465        else
466        {
467            hasSameStepTarget = true;
468        }
469        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
470        for (ConditionalResultDescriptor result : conditionalResults)
471        {
472            StepDescriptor conditionalStep = workflowDescriptor.getStep(result.getStep());
473            if (conditionalStep != null)
474            {
475                outgoingSteps.add(conditionalStep);
476            }
477            else
478            {
479                hasSameStepTarget = true;
480            }
481        }
482        if (hasSameStepTarget)
483        {
484            outgoingSteps.addAll(getIncomingSteps(action.getId(), workflowDescriptor));
485        }
486        
487        return outgoingSteps;
488    }
489    
490    /**
491     * Get possible incoming steps for action
492     * @param actionId the current action's id
493     * @param workflowDescriptor the current  workflow
494     * @return a set of the action's incoming steps
495     */
496    public Set<StepDescriptor> getIncomingSteps(int actionId, WorkflowDescriptor workflowDescriptor)
497    {
498        Set<StepDescriptor> incomingSteps = new HashSet<>();
499        List<StepDescriptor> steps = workflowDescriptor.getSteps();
500        for (StepDescriptor step : steps)
501        {
502            if (step.getAction(actionId) != null)
503            {
504                incomingSteps.add(step);
505            }
506        }
507        return incomingSteps;
508    }
509    
510    /**
511     * Get current action's final steps and associated conditions
512     * @param currentNode id of current node
513     * @param workflowName unique name of current workflow
514     * @param actionId id of current action
515     * @return a map of current node's children
516     */
517    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
518    public Map<String, Object> getFinalSteps(String currentNode, String workflowName, Integer actionId)
519    {
520        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
521        List<Map<String, Object>> nodes = new ArrayList<>();
522        if (_workflowRightHelper.canRead(workflowDescriptor))
523        {
524            ActionDescriptor action = workflowDescriptor.getAction(actionId);
525            boolean canWrite = _workflowRightHelper.canWrite(workflowDescriptor);
526            
527            List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
528            
529            if (currentNode.equals("root")) //get results(steps) nodes
530            {
531                ResultDescriptor unconditionalResult = action.getUnconditionalResult();
532                int stepId = unconditionalResult.getStep();
533                Map<String, Object> step2json = _step2JSON(workflowDescriptor, stepId, false, stepId != -1, canWrite);
534                step2json.put("isConditional", false);
535                nodes.add(step2json);
536                
537                for (ConditionalResultDescriptor result : conditionalResults)
538                {
539                    stepId = result.getStep();
540                    Map<String, Object> conditionalStep2Json = _step2JSON(workflowDescriptor, stepId, !result.getConditions().isEmpty(), stepId != -1, canWrite);
541                    conditionalStep2Json.put("isConditional", true);
542                    nodes.add(conditionalStep2Json);
543                }
544            }
545            else //get conditions nodes,
546            {
547                //conditions to display
548                List<AbstractDescriptor> conditions = _workflowResultDAO.getChildrenResultConditions(currentNode, action, conditionalResults);
549                if (!conditions.isEmpty())
550                {
551                    String[] path = _workflowResultDAO.getPath(currentNode);
552                    int stepId = Integer.valueOf(path[0].substring(4));
553                    ConditionsDescriptor rootOperator = _workflowResultDAO.getRootResultConditions(conditionalResults, stepId).get(0);
554                    boolean rootIsAND = !rootOperator.getType().equals(WorkflowConditionDAO.OR);
555                    for (int i = 0; i < conditions.size(); i++)
556                    {
557                        nodes.add(_workflowConditionDAO.conditionToJSON(conditions.get(i), currentNode, i, rootIsAND));
558                    }
559                }
560            }
561        }
562        
563        return Map.of("results", nodes);
564    }
565    
566    /**
567     * Get step infos
568     * @param workflowDescriptor current workflow
569     * @param stepId id of current step
570     * @param hasChildren true if step has actions
571     * @param showId true if id needs to be displayed in the label
572     * @param canWrite true if current user has edition right on current workflow
573     * @return a map of the step infos
574     */
575    protected Map<String, Object> _step2JSON(WorkflowDescriptor workflowDescriptor, int stepId, boolean hasChildren, boolean showId, boolean canWrite)
576    {
577        Map<String, Object> infos = new HashMap<>();
578        
579        infos.put("id", "step" + stepId);
580        infos.put("elementId", stepId);
581        infos.put("label", getStepLabelAsString(workflowDescriptor, stepId, showId));
582        infos.put("elementType", "step");
583        infos.put("hasChildren", hasChildren);
584        infos.put("canWrite", canWrite);
585        try
586        {
587            infos.put("smallIcon", getStepIconPath(workflowDescriptor, stepId));
588        }
589        catch (Exception e)
590        {
591            getLogger().error("An error occurred while getting icon path for step id {}", stepId, e);
592        }
593       
594        return infos;
595    }
596    
597    /**
598     * Get the workflow's steps available as unconditional result for actions
599     * @param workflowName the current workflow name
600     * @param actionId id of current action if exist, can be null
601     * @param isInitialState true if current selected state is the initial state
602     * @return a map of the workflow steps
603     */
604    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
605    public Map<String, Object> getStatesToJson(String workflowName, Integer actionId, Boolean isInitialState)
606    {
607        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
608        _workflowRightHelper.checkReadRight(workflowDescriptor);
609        List<Map<String, Object>> states = new ArrayList<>();
610        List<Integer> conditionalStepIds = new ArrayList<>();
611        List<StepDescriptor> steps = workflowDescriptor.getSteps();
612        if (actionId != null)
613        {
614            ActionDescriptor action = workflowDescriptor.getAction(actionId);
615            List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
616            for (ConditionalResultDescriptor conditionalResult : conditionalResults)
617            {
618                conditionalStepIds.add(conditionalResult.getStep());
619            }
620        }
621        for (StepDescriptor step : steps)
622        {
623            int stepId = step.getId();
624            if (!conditionalStepIds.contains(stepId))
625            {
626                Map<String, Object> stateInfos = new HashMap<>();
627                stateInfos.put("id", stepId);
628                stateInfos.put("label", getStepLabelAsString(workflowDescriptor, stepId, true));
629                states.add(stateInfos);
630            }
631        }
632        if (!isInitialState && !conditionalStepIds.contains(-1))
633        {
634            //Same state
635            Map<String, Object> stateInfos = new HashMap<>();
636            stateInfos.put("id", -1);
637            stateInfos.put("label", new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_RESULTS_SAME_STEP"));
638            states.add(stateInfos);
639        }
640        
641        return Map.of("data", states);
642    }
643    
644    
645    /**
646     * Get the translated step label
647     * @param workflowDescriptor current workflow
648     * @param stepId id of current step
649     * @param showId true if id needs to be displayed in the label
650     * @return the step label as string
651     */
652    public String getStepLabelAsString(WorkflowDescriptor workflowDescriptor, int stepId, boolean showId)
653    {
654        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
655        return showId
656                ? _i18nHelper.translateKey(workflowDescriptor.getName(), label, DEFAULT_STEP_NAME) + " (" + stepId + ")"
657                : _i18nHelper.translateKey(workflowDescriptor.getName(), label, DEFAULT_STEP_NAME);
658    }
659    
660    /**
661     * Get the step's icon path
662     * @param workflowDescriptor current worklfow
663     * @param stepId id of current step
664     * @return the icon path
665     */
666    public String getStepIconPath(WorkflowDescriptor workflowDescriptor, int stepId)
667    {
668        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
669        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowDescriptor.getName(), label);
670        return _workflowHelper.getElementIconPath(label, __DEFAULT_STEP_ICON_PATH);
671    }
672    
673    /**
674     * Get the step's icon path as base 64 for svg links
675     * @param workflowDescriptor current worklfow
676     * @param stepId id of current step
677     * @return the icon path as base 64
678     */
679    public String getStepIconPathAsBase64(WorkflowDescriptor workflowDescriptor, int stepId)
680    {
681        I18nizableText label = getStepLabel(workflowDescriptor, stepId);
682        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowDescriptor.getName(), label);
683        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_STEP_ICON_PATH);
684    }
685    
686    /**
687     * Get the step i18n label
688     * @param workflowDescriptor current workflow
689     * @param stepId id of current step
690     * @return the i18n step label
691     */
692    public I18nizableText getStepLabel(WorkflowDescriptor workflowDescriptor, int stepId)
693    {
694        switch (stepId)
695        {
696            case -1:
697                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_RESULTS_SAME_STEP");
698            case 0:
699                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_INITIAL_STEP_NAME");
700            default:
701                StepDescriptor step = workflowDescriptor.getStep(stepId);
702                return getStepLabel(step);
703        }
704    }
705    
706    /**
707     * Get a list of actions outgoing from current step
708     * @param stepId id of current step
709     * @param workflow current workflow
710     * @return the list of outgoing actions
711     */
712    public List<ActionDescriptor> getOutgoingActions(int stepId, WorkflowDescriptor workflow)
713    {
714        return stepId != 0
715                ? workflow.getStep(stepId).getActions()
716                : workflow.getInitialActions();
717    }
718    
719    /**
720     * Get a set of actions incoming to current step
721     * @param stepId id of current step
722     * @param workflow current workflow
723     * @return the set of outgoing actions
724     */
725    @SuppressWarnings("unchecked")
726    public Set<ActionDescriptor> getIncomingActions(int stepId , WorkflowDescriptor workflow)
727    {
728        Set<ActionDescriptor> incomingActions = new HashSet<>();
729        if (stepId != 0)
730        {
731            incomingActions.addAll(_getIncomingActionsFromList(stepId, workflow.getInitialActions()));
732            List<StepDescriptor> steps = workflow.getSteps();
733            for (StepDescriptor otherSteps : steps)
734            {
735                if (otherSteps.getId() != stepId)
736                {
737                    List<ActionDescriptor> actions = otherSteps.getActions();
738                    incomingActions.addAll(_getIncomingActionsFromList(stepId, actions));
739                }
740            }
741        }
742        return incomingActions;
743    }
744    
745    /**
746     * Get a set of incoming actions if present in actions list
747     * @param stepId id of current step
748     * @param actions list of other step's actions
749     * @return a list containing other step's outgoing actions that are incoming to current step
750     */
751    protected Set<ActionDescriptor> _getIncomingActionsFromList(int stepId, List<ActionDescriptor> actions)
752    {
753        Set<ActionDescriptor> incoming = new HashSet<>();
754        for (ActionDescriptor action : actions)
755        {
756            ResultDescriptor unconditionalResult = action.getUnconditionalResult();
757            if (unconditionalResult.getStep() == stepId)
758            {
759                incoming.add(action);
760            }
761            else
762            {
763                boolean leadToStep = false;
764                List<ResultDescriptor> conditionalResults = action.getConditionalResults();
765                int indexResult = 0;
766                while (!leadToStep && indexResult < conditionalResults.size())
767                {
768                    if (conditionalResults.get(indexResult).getStep() == stepId)
769                    {
770                        incoming.add(action);
771                        leadToStep = true;
772                    }
773                    indexResult++;
774                }
775            }
776        }
777        return incoming;
778    }
779    
780    /**
781     * Get id of the first step having current action, INITIAL_STEP_ID if current action is an initial action
782     * @param stepId id of current step
783     * @param steps list of all the steps in current workflow
784     * @param actionId id of current action
785     * @return the id of the first found step having current action
786     */
787    public String getFirstParentStepId(int stepId, List<StepDescriptor> steps, Integer actionId)
788    {
789        String firstParentStepId = "";
790        int i = 0;
791        do
792        {
793            StepDescriptor otherStep = steps.get(i);
794            if (otherStep.getId() != stepId && otherStep.getAction(actionId) != null)
795            {
796                firstParentStepId = String.valueOf(otherStep.getId());
797            }
798            i++;
799        } while (firstParentStepId.isEmpty() && i < steps.size());
800        return firstParentStepId.isBlank() ? "0" : firstParentStepId;
801    }
802    
803    private record StepWithIcon(Integer id, String label, String iconPath) { /* empty */ }
804}