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 */
016
017package org.ametys.plugins.workflow.dao;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Map.Entry;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.ProcessingException;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.lang3.tuple.Pair;
035
036import org.ametys.core.ui.Callable;
037import org.ametys.plugins.workflow.EnhancedCondition;
038import org.ametys.plugins.workflow.EnhancedConditionExtensionPoint;
039import org.ametys.plugins.workflow.ModelItemTypeExtensionPoint;
040import org.ametys.plugins.workflow.component.WorkflowArgument;
041import org.ametys.plugins.workflow.support.AvalonTypeResolver;
042import org.ametys.plugins.workflow.support.WorflowRightHelper;
043import org.ametys.plugins.workflow.support.WorkflowElementDefinitionHelper;
044import org.ametys.plugins.workflow.support.WorkflowHelper;
045import org.ametys.plugins.workflow.support.WorkflowHelper.WorkflowVisibility;
046import org.ametys.plugins.workflow.support.WorkflowSessionHelper;
047import org.ametys.runtime.i18n.I18nizableText;
048import org.ametys.runtime.model.DefinitionContext;
049import org.ametys.runtime.model.ElementDefinition;
050import org.ametys.runtime.model.Model;
051import org.ametys.runtime.model.SimpleViewItemGroup;
052import org.ametys.runtime.model.StaticEnumerator;
053import org.ametys.runtime.model.View;
054import org.ametys.runtime.model.ViewElement;
055import org.ametys.runtime.model.disableconditions.DisableCondition.OPERATOR;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057
058import com.opensymphony.workflow.Condition;
059import com.opensymphony.workflow.TypeResolver;
060import com.opensymphony.workflow.WorkflowException;
061import com.opensymphony.workflow.loader.AbstractDescriptor;
062import com.opensymphony.workflow.loader.ActionDescriptor;
063import com.opensymphony.workflow.loader.ConditionDescriptor;
064import com.opensymphony.workflow.loader.ConditionalResultDescriptor;
065import com.opensymphony.workflow.loader.ConditionsDescriptor;
066import com.opensymphony.workflow.loader.DescriptorFactory;
067import com.opensymphony.workflow.loader.RestrictionDescriptor;
068import com.opensymphony.workflow.loader.WorkflowDescriptor;
069
070/**
071 * DAO for workflow conditions
072 */
073public class WorkflowConditionDAO extends AbstractLogEnabled implements Component, Serviceable
074{
075    /** The component role */ 
076    public static final String ROLE = WorkflowConditionDAO.class.getName();
077    
078    /** The "and" type of condition */
079    public static final String AND = "AND";
080    
081    /** The "or" type of condition */
082    public static final String OR = "OR";
083    
084    /** Key for "and" label in tree  */
085    protected static final String __ANDI18N = "PLUGIN_WORKFLOW_TRANSITION_CONDITIONS_TYPE_AND";
086    
087    /** Key for "or" label in tree  */
088    protected static final String __ORI18N = "PLUGIN_WORKFLOW_TRANSITION_CONDITIONS_TYPE_OR";
089    
090    /** Extension point for workflow arguments data type */
091    protected static ModelItemTypeExtensionPoint _workflowArgumentDataTypeExtensionPoint;
092    
093    private static final String __OR_ROOT_OPERATOR = "or0";
094    private static final String __AND_ROOT_OPERATOR = "and0";
095    private static final String __STEP_RESULT_PREFIX = "step";
096    private static final String __ROOT_RESULT_ID = "root";
097    private static final String __ATTRIBUTE_CONDITIONS_LIST = "conditions-list";
098    
099    /** The workflow session helper */
100    protected WorkflowSessionHelper _workflowSessionHelper;
101    
102    /** The workflow helper */
103    protected WorkflowHelper _workflowHelper;
104    
105    /** The workflow right helper */
106    protected WorflowRightHelper _workflowRightHelper;
107    
108    /** The workflow result helper */
109    protected WorkflowResultDAO _workflowResultDAO;
110    
111    /** The workflow step DAO */
112    protected WorkflowStepDAO _workflowStepDAO;
113    
114    /** The workflow transition DAO */
115    protected WorkflowTransitionDAO _workflowTransitionDAO;
116    
117    /** Extension point for Conditions */
118    protected EnhancedConditionExtensionPoint _enhancedConditionExtensionPoint;
119    
120    /** The service manager */
121    protected ServiceManager _manager;
122
123    public void service(ServiceManager smanager) throws ServiceException
124    {
125        _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE);
126        _workflowRightHelper = (WorflowRightHelper) smanager.lookup(WorflowRightHelper.ROLE);
127        _workflowSessionHelper = (WorkflowSessionHelper) smanager.lookup(WorkflowSessionHelper.ROLE);
128        _workflowResultDAO = (WorkflowResultDAO) smanager.lookup(WorkflowResultDAO.ROLE);
129        _workflowStepDAO = (WorkflowStepDAO) smanager.lookup(WorkflowStepDAO.ROLE);
130        _workflowTransitionDAO = (WorkflowTransitionDAO) smanager.lookup(WorkflowTransitionDAO.ROLE);
131        _workflowArgumentDataTypeExtensionPoint = (ModelItemTypeExtensionPoint) smanager.lookup(ModelItemTypeExtensionPoint.ROLE_WORKFLOW);
132        _enhancedConditionExtensionPoint = (EnhancedConditionExtensionPoint) smanager.lookup(EnhancedConditionExtensionPoint.ROLE);
133        _manager = smanager;
134    }
135    
136    /**
137     * Get the condition's model items as fields to configure edition form panel
138     * @return the parameters field as Json readable map
139     * @throws ProcessingException exception while saxing view to json
140     */
141    @Callable(rights = {"Workflow_Right_Edit", "Workflow_Right_Edit_User"})
142    public Map<String, Object> getConditionsModel() throws ProcessingException
143    {
144        Map<String, Object> response = new HashMap<>();
145        
146        View view = new View();
147        SimpleViewItemGroup fieldset = new SimpleViewItemGroup();
148        fieldset.setName("conditions");
149        
150        Set<Pair<String, EnhancedCondition>> conditions = _enhancedConditionExtensionPoint.getAllConditions()
151                .stream()
152                .filter(this::_hasConditionRight)
153                .collect(Collectors.toSet());
154        
155        List<ElementDefinition> modelItems = new ArrayList<>();
156        modelItems.add(_getConditionsListModelItem(conditions));
157        modelItems.addAll(_getArgumentModelItems(conditions));
158        Model.of("conditions", "worklow", modelItems.toArray(new ElementDefinition[modelItems.size()]));
159        
160        for (ElementDefinition conditionArgument : modelItems)
161        {
162            ViewElement argumentView = new ViewElement();
163            argumentView.setDefinition(conditionArgument);
164            fieldset.addViewItem(argumentView);
165        }
166        
167        view.addViewItem(fieldset);
168
169        response.put("parameters", view.toJSON(DefinitionContext.newInstance().withEdition(true)));
170        
171        return response;
172    }
173    
174    private boolean _hasConditionRight(Pair<String, EnhancedCondition> condition)
175    {
176        List<WorkflowVisibility> conditionVisibilities = condition.getRight().getVisibilities();
177        if (conditionVisibilities.contains(WorkflowVisibility.USER))
178        {
179            return _workflowRightHelper.hasEditUserRight();
180        }
181        else if (conditionVisibilities.contains(WorkflowVisibility.SYSTEM))
182        {
183            return _workflowRightHelper.hasEditSystemRight();
184        }
185        return false;
186    }
187
188    
189    /**
190     * Get the model item for the list of conditions
191     * @param conditions a list of all the conditions with their ids
192     * @return an enum of the conditions as a model item 
193     */
194    private ElementDefinition<String> _getConditionsListModelItem(Set<Pair<String, EnhancedCondition>> conditions)
195    {
196        ElementDefinition<String> conditionsList = WorkflowElementDefinitionHelper.getElementDefinition(
197            __ATTRIBUTE_CONDITIONS_LIST,
198            new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_ADD_CONDITION_DIALOG_ROLE"),
199            new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_ADD_CONDITION_DIALOG_ROLE_DESC"),
200            true,
201            false
202        );
203        
204        StaticEnumerator<String> conditionStaticEnumerator = new StaticEnumerator<>();
205        for (Pair<String, EnhancedCondition> condition : conditions)
206        {
207            conditionStaticEnumerator.add(condition.getRight().getLabel(), condition.getLeft());
208        }
209        conditionsList.setEnumerator(conditionStaticEnumerator);
210       
211        return conditionsList;
212    }
213
214    /**
215     * Get a list of workflow arguments model items with disable conditions on non related function selected
216     * @param conditions a list of all the conditions with their ids
217     * @return the list of model items
218     */
219    protected List<WorkflowArgument> _getArgumentModelItems(Set<Pair<String, EnhancedCondition>> conditions)
220    {
221        List<WorkflowArgument> argumentModelItems = new ArrayList<>();
222        for (Pair<String, EnhancedCondition> condition : conditions)
223        {
224            String conditionId = condition.getLeft();
225            for (WorkflowArgument arg : condition.getRight().getArguments())
226            {
227                arg.setName(conditionId + "-" + arg.getName());
228                WorkflowElementDefinitionHelper.addRelativeDisableCondition(arg, __ATTRIBUTE_CONDITIONS_LIST, OPERATOR.NEQ, conditionId);
229                argumentModelItems.add(arg);
230            }
231        }
232        return argumentModelItems;
233    }
234    
235    /**
236     * Get current argument's values for condition
237     * @param workflowName unique name of current workflow
238     * @param actionId id of current action
239     * @param conditionNodeId id of selected node
240     * @return a map of the condition's arguments and their current values
241     * @throws ProcessingException exception while resolving condition
242     */
243    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
244    public Map<String, Object> getConditionParametersValues(String workflowName, Integer actionId, String conditionNodeId) throws ProcessingException
245    {
246        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
247        _workflowRightHelper.checkEditRight(workflowDescriptor);
248
249        ConditionDescriptor condition = _getCondition(workflowDescriptor, actionId, conditionNodeId);
250        Map<String, String> args = new HashMap<>();
251        Map<String, String> conditionCurrentArguments = condition.getArgs();
252        String conditionId = conditionCurrentArguments.get("id");
253        for (Entry<String, String> entry : conditionCurrentArguments.entrySet())
254        {
255            String argName = entry.getKey().equals("id") ? __ATTRIBUTE_CONDITIONS_LIST : conditionId + "-" + entry.getKey();
256            args.put(argName, entry.getValue());
257        }
258        args.putAll(conditionCurrentArguments);
259
260        Map<String, Object> results = new HashMap<>();
261        results.put("parametersValues", args);
262        return results;
263    }
264
265    /**
266     * Get the current condition as condition descriptor
267     * @param workflowDescriptor current workflow
268     * @param actionId id of current action
269     * @param conditionNodeId id of current node
270     * @return the conditions
271     */
272    protected ConditionDescriptor _getCondition(WorkflowDescriptor workflowDescriptor, Integer actionId, String conditionNodeId)
273    {
274        ActionDescriptor action = workflowDescriptor.getAction(actionId);
275        
276        String[] path = _workflowResultDAO.getPath(conditionNodeId);
277        int nodeToEdit = Integer.valueOf(path[path.length - 1].substring("condition".length()));
278        boolean isResult = path[0].startsWith(__STEP_RESULT_PREFIX);
279        
280        ConditionsDescriptor rootOperator = isResult 
281                ? (ConditionsDescriptor) _workflowResultDAO.getRootResultConditions(action.getConditionalResults(), _getStepId(conditionNodeId)).get(0)
282                : action.getRestriction().getConditionsDescriptor();
283        String parentNodeId = conditionNodeId.substring(0, conditionNodeId.lastIndexOf('-'));
284        ConditionsDescriptor parentNode = _getConditionsNode(rootOperator, parentNodeId);
285        ConditionDescriptor condition = (ConditionDescriptor) parentNode.getConditions().get(nodeToEdit);
286        
287        return condition;
288    }
289    
290    /**
291     * Edit the condition
292     * @param workflowName unique name of current workflow
293     * @param stepId id of step parent
294     * @param actionId id of current action
295     * @param conditionNodeId id of selected node
296     * @param arguments map of arguments name and values
297     * @return map of condition's info
298     * @throws WorkflowException exception while resolving condition
299     */
300    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
301    public Map<String, Object> editCondition(String workflowName, Integer stepId, Integer actionId, String conditionNodeId, Map<String, Object> arguments) throws WorkflowException
302    {
303        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
304        
305        // Check user right
306        _workflowRightHelper.checkEditRight(workflowDescriptor);
307        
308        ConditionDescriptor condition = _getCondition(workflowDescriptor, actionId, conditionNodeId);
309        _updateArguments(condition, arguments);
310        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
311        
312        ActionDescriptor action = workflowDescriptor.getAction(actionId);
313        return _getConditionProperties(workflowDescriptor, action, condition, stepId, conditionNodeId);
314    }
315    
316    private Map<String, Object> _getConditionProperties(WorkflowDescriptor workflow, ActionDescriptor action, ConditionDescriptor condition, Integer stepId, String conditionNodeId)
317    {
318        Map<String, Object> results = new HashMap<>();
319        results.put("conditionId", condition.getArgs().get("id"));
320        results.put("nodeId", conditionNodeId);
321        results.put("actionId", action.getId());
322        results.put("actionLabel", _workflowTransitionDAO.getActionLabel(action));
323        results.put("stepId", stepId);
324        results.put("stepLabel", _workflowStepDAO.getStepLabel(workflow, stepId));
325        results.put("workflowId", workflow.getName());
326        return results;
327    }
328    
329    private void _updateArguments(ConditionDescriptor condition, Map<String, Object> arguments)
330    {
331        Map<String, String> args = condition.getArgs();
332        args.clear();
333        args.putAll(_getConditionParamsValuesAsString(arguments));
334    }
335    
336    /**
337     * Add a new condition
338     * @param workflowName unique name of current workflow
339     * @param stepId id of step parent
340     * @param actionId id of current action
341     * @param parentNodeId id of selected node : can be null if no node is selected
342     * @param arguments map of arguments name and values
343     * @return map of condition's info
344     */
345    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
346    public Map<String, Object> addCondition(String workflowName, Integer stepId, Integer actionId, String parentNodeId, Map<String, Object> arguments) 
347    {
348        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
349        
350        // Check user right
351        _workflowRightHelper.checkEditRight(workflowDescriptor);
352
353        ActionDescriptor action = workflowDescriptor.getAction(actionId);
354
355        boolean isResultCondition = !StringUtils.isBlank(parentNodeId) && parentNodeId.startsWith(__STEP_RESULT_PREFIX);
356        String computedParentNodeId = StringUtils.isBlank(parentNodeId) 
357                ? __AND_ROOT_OPERATOR 
358                : isResultCondition && parentNodeId.split("-").length == 1
359                    ? parentNodeId + "-" + __AND_ROOT_OPERATOR
360                    : parentNodeId;
361        ConditionsDescriptor rootOperator = isResultCondition
362                ? _getResultConditionsRootOperator(workflowName, stepId, actionId, parentNodeId, action)
363                : _getConditionsRootOperator(workflowName, stepId, actionId, action, parentNodeId);
364       
365        ConditionsDescriptor currentParentOperator = _getConditionsNode(rootOperator, computedParentNodeId);
366        if (StringUtils.isBlank(currentParentOperator.getType()))
367        {
368            currentParentOperator.setType(AND);
369        }
370        
371        
372        List<AbstractDescriptor> conditions = currentParentOperator.getConditions();
373        ConditionDescriptor conditionDescriptor = _createCondition(arguments);
374        conditions.add(conditionDescriptor);
375
376        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
377        
378        String conditionNodeId = computedParentNodeId + "-" + "condition" + (conditions.size() - 1);
379        return _getConditionProperties(workflowDescriptor, action, conditionDescriptor, stepId, conditionNodeId);
380    }
381    
382    private ConditionDescriptor _createCondition(Map<String, Object> arguments)
383    {
384        DescriptorFactory factory = new DescriptorFactory();
385        ConditionDescriptor conditionDescriptor = factory.createConditionDescriptor();
386        conditionDescriptor.setType("avalon");
387        _updateArguments(conditionDescriptor, arguments);
388        return conditionDescriptor;
389    }
390    
391    /**
392     * Get the list of arguments as String, parse multiple arguments
393     * @param params List of condition arguments with values
394     * @return the map of arguments formated for condition descriptor
395     */
396    protected Map<String, String> _getConditionParamsValuesAsString(Map<String, Object> params)
397    {
398        String conditionId = (String) params.get(__ATTRIBUTE_CONDITIONS_LIST);
399        EnhancedCondition enhancedCondition = _enhancedConditionExtensionPoint.getExtension(conditionId);
400        
401        Map<String, String> conditionParams = new HashMap<>();
402        conditionParams.put("id", conditionId);
403        List<WorkflowArgument> arguments = enhancedCondition.getArguments();
404        if (!arguments.isEmpty())
405        {
406            for (WorkflowArgument argument : arguments)
407            {
408                String paramKey = conditionId + "-" + argument.getName();
409                String paramValue = argument.isMultiple()
410                        ?  _getListAsString(params, paramKey)
411                        : (String) params.get(paramKey);
412                if (!StringUtils.isBlank(paramValue))
413                {
414                    conditionParams.put(argument.getName(), paramValue);
415                }
416            }
417        }
418        return conditionParams;
419    }
420    
421    @SuppressWarnings("unchecked")
422    private String _getListAsString(Map<String, Object> params, String paramKey)
423    {
424        List<String> values = (List<String>) params.get(paramKey);
425        return values == null ? "" : String.join(",", values);
426    }
427    
428    /**
429     * Get the root operator for regular conditions
430     * @param workflowName name of current workflow
431     * @param stepId id of step parent
432     * @param actionId id of current action
433     * @param action the current action
434     * @param parentNodeId id of the parent node
435     * @return the root conditions operator
436     */
437    protected ConditionsDescriptor _getConditionsRootOperator(String workflowName, Integer stepId, Integer actionId, ActionDescriptor action, String parentNodeId)
438    {
439        RestrictionDescriptor restriction = action.getRestriction();
440        if (StringUtils.isBlank(parentNodeId)) //root
441        {
442            if (restriction == null)
443            {
444                addOperator(workflowName, stepId, actionId, parentNodeId, AND, false);
445                restriction = action.getRestriction();
446            }
447        }
448        
449        return restriction.getConditionsDescriptor();
450    }
451    
452    /**
453     * Get the root operator for result conditions
454     * @param workflowName name of current workflow
455     * @param stepId id of step parent
456     * @param actionId id of current action
457     * @param action the current action
458     * @param parentNodeId id of the parent node
459     * @return the root conditions operator
460     */
461    protected ConditionsDescriptor _getResultConditionsRootOperator(String workflowName, Integer stepId, Integer actionId, String parentNodeId, ActionDescriptor action)
462    {
463        int resultStepId = _getStepId(parentNodeId);
464        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
465        List<ConditionsDescriptor> resultConditions = _workflowResultDAO.getRootResultConditions(conditionalResults, resultStepId);
466        if (resultConditions.isEmpty())
467        {
468            addOperator(workflowName, stepId, actionId, parentNodeId, AND, true);
469            resultConditions = _workflowResultDAO.getRootResultConditions(conditionalResults, resultStepId);
470        }
471        
472        return resultConditions.get(0);
473    }
474    
475    /**
476     * Add an operator
477     * @param workflowName unique name of current workflow
478     * @param stepId id of step parent
479     * @param actionId id of current action
480     * @param nodeId id of selected node
481     * @param operatorType the workflow conditions' type. Can be AND or OR 
482     * @param isResultCondition true if parent is a conditional result
483     * @return map of the operator's infos
484     */
485    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
486    public Map<String, Object> addOperator(String workflowName, Integer stepId, Integer actionId, String nodeId, String operatorType, boolean isResultCondition)
487    {
488        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
489        
490        // Check user right
491        _workflowRightHelper.checkEditRight(workflowDescriptor);
492        
493        ActionDescriptor action = workflowDescriptor.getAction(actionId);
494        
495        DescriptorFactory factory = new DescriptorFactory();
496        ConditionsDescriptor newOperator = factory.createConditionsDescriptor();
497        newOperator.setType(operatorType);
498        boolean isRoot = StringUtils.isBlank(nodeId);
499        
500        String newNodeId = "";
501        if (isResultCondition)
502        {
503            String[] path = _workflowResultDAO.getPath(nodeId);
504            List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
505            List<ConditionsDescriptor> resultConditions = _workflowResultDAO.getRootResultConditions(conditionalResults, _getStepId(nodeId));
506            if (resultConditions.isEmpty())//root
507            {
508                resultConditions.add(newOperator);
509                newNodeId =  nodeId + "-" + operatorType.toLowerCase() + "0";
510            }
511            else
512            {
513                isRoot =  path.length == 1;
514                ConditionsDescriptor rootOperator = resultConditions.get(0);
515                
516                ConditionsDescriptor currentOperator = isRoot ? rootOperator : _getConditionsNode(rootOperator, nodeId);
517                List<AbstractDescriptor> conditions = currentOperator.getConditions();
518                conditions.add(newOperator);
519                String parentNodePrefix = isRoot ? nodeId + "-" + __AND_ROOT_OPERATOR : nodeId;
520                newNodeId = parentNodePrefix + "-" + operatorType.toLowerCase() + (conditions.size() - 1);
521            }
522        }
523        else
524        {
525            if (isRoot && action.getRestriction() ==  null) //root with no condition underneath
526            {
527                RestrictionDescriptor restriction = new RestrictionDescriptor();
528                restriction.setConditionsDescriptor(newOperator);
529                action.setRestriction(restriction);
530                newNodeId = operatorType.toLowerCase() + "0";
531            }
532            else
533            {
534                RestrictionDescriptor restriction = action.getRestriction();
535                ConditionsDescriptor rootOperator = restriction.getConditionsDescriptor();
536
537                ConditionsDescriptor currentOperator = isRoot ? rootOperator : _getConditionsNode(rootOperator, nodeId);
538                List<AbstractDescriptor> conditions = currentOperator.getConditions();
539                conditions.add(newOperator);
540                String parentNodePrefix = isRoot ? __AND_ROOT_OPERATOR : nodeId;
541                newNodeId = parentNodePrefix + "-" + operatorType.toLowerCase() + (conditions.size() - 1);
542            }
543        }
544        
545        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
546        
547        return _getOperatorProperties(workflowDescriptor, action, stepId, operatorType, newNodeId);
548    }
549
550    private Map<String, Object> _getOperatorProperties(WorkflowDescriptor workflowDescriptor, ActionDescriptor action, Integer stepId, String operatorType, String newNodeId)
551    {
552        Map<String, Object> results = new HashMap<>();
553        results.put("nodeId", newNodeId);
554        results.put("type", operatorType);
555        results.put("actionId", action.getId());
556        results.put("actionLabel", _workflowTransitionDAO.getActionLabel(action));
557        results.put("stepId", stepId);
558        results.put("stepLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, stepId));
559        results.put("workflowId", workflowDescriptor.getName());
560        return results;
561    }
562
563    private Integer _getStepId(String nodeId)
564    {
565        String[] path = _workflowResultDAO.getPath(nodeId);
566        return Integer.valueOf(path[0].substring(4));
567    }
568    
569    private ConditionsDescriptor _getConditionsNode(ConditionsDescriptor rootOperator, String nodeId)
570    {
571        ConditionsDescriptor currentOperator = rootOperator;
572        
573        List<AbstractDescriptor> conditions = rootOperator.getConditions();
574        String[] path = _workflowResultDAO.getPath(nodeId);
575        boolean isResultCondition = path[0].startsWith(__STEP_RESULT_PREFIX);
576        if (!isResultCondition && path.length > 1 || isResultCondition && path.length > 2) //current node is not root direct child
577        {
578            // get conditions for current node
579            int i = isResultCondition ? 2 : 1;
580            do
581            {
582                int currentOrAndConditionIndex = _getConditionIndex(path, i);
583                
584                currentOperator = (ConditionsDescriptor) conditions.get(currentOrAndConditionIndex);
585                conditions = currentOperator.getConditions();
586                i++;
587            }
588            while (i < path.length);
589        }
590        return currentOperator;
591    }
592    
593    private int _getConditionIndex(String[] path, int conditionIndex)
594    {
595        String currentConditionId = path[conditionIndex];
596        int prefixSize = currentConditionId.startsWith("and")
597                ? "and".length()
598                : currentConditionId.startsWith("or")
599                    ? "or".length()
600                    : "condition".length();
601        return Integer.valueOf(currentConditionId.substring(prefixSize));
602    }
603    
604    /**
605     * Delete operator from action
606     * @param workflowName unique name of current workflow
607     * @param stepId id of step parent
608     * @param actionId id of current action
609     * @param nodeId id of selected node
610     * @return map of operator's infos
611     */
612    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
613    public Map<String, Object> deleteOperator(String workflowName, Integer stepId, Integer actionId, String nodeId)
614    {
615        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
616        
617        // Check user right
618        _workflowRightHelper.checkEditRight(workflowDescriptor);
619        
620        ActionDescriptor action = workflowDescriptor.getAction(actionId);
621        
622        String[] path = _workflowResultDAO.getPath(nodeId);
623        boolean isResultCondition = nodeId.startsWith(__STEP_RESULT_PREFIX);
624        if (isResultCondition)
625        {
626            _removeResultConditionOperator(nodeId, action, path);
627        }
628        else
629        {
630            _removeConditionOperator(nodeId, action, path);
631        }
632        
633        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
634        
635        String type = path[path.length - 1].startsWith("and") ? AND : OR;
636        return _getOperatorProperties(workflowDescriptor, action, stepId, type, nodeId);
637    }
638
639    /**
640     * Remove a regular condition operator 
641     * @param nodeId id of operator to remove
642     * @param action current action
643     * @param path the path to current node
644     */
645    protected void _removeConditionOperator(String nodeId, ActionDescriptor action, String[] path)
646    {
647        if (path.length > 1)
648        {
649            ConditionsDescriptor rootCondition = action.getRestriction().getConditionsDescriptor();
650            _removeCondition(rootCondition, nodeId, path);
651        }
652        else
653        {
654            action.setRestriction(null);
655        }
656    }
657
658    /**
659     * Remove a result condition operator 
660     * @param nodeId id of operator to remove
661     * @param action current action
662     * @param path the path to current node
663     */
664    protected void _removeResultConditionOperator(String nodeId, ActionDescriptor action, String[] path)
665    {
666        if (path.length > 2)
667        {
668            ConditionsDescriptor rootCondition = (ConditionsDescriptor) _workflowResultDAO.getRootResultConditions(action.getConditionalResults(), _getStepId(nodeId)).get(0);
669            _removeCondition(rootCondition, nodeId, path);
670        }
671        else
672        {
673            Optional parentConditionalResult = action.getConditionalResults().stream()
674                    .filter(cr -> ((ConditionalResultDescriptor) cr).getStep() == _getStepId(nodeId))
675                    .findFirst();
676            if (parentConditionalResult.isPresent())
677            {
678                ((ConditionalResultDescriptor) parentConditionalResult.get()).getConditions().clear();
679            }
680        }
681    }
682
683    /**
684     * Delete the condition
685     * @param workflowName unique name of current workflow
686     * @param stepId id of step parent
687     * @param actionId id of current action
688     * @param nodeId id of selected node
689     * @return map of the condition's infos
690     */
691    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
692    public Map<String, Object> deleteCondition(String workflowName, Integer stepId, Integer actionId, String nodeId)
693    {
694        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
695        
696        // Check user right
697        _workflowRightHelper.checkEditRight(workflowDescriptor);
698        
699        ActionDescriptor action = workflowDescriptor.getAction(actionId);
700        
701        String[] path = _workflowResultDAO.getPath(nodeId);
702        boolean isResultCondition = nodeId.startsWith(__STEP_RESULT_PREFIX);
703        ConditionsDescriptor rootCondition = isResultCondition 
704                ? (ConditionsDescriptor) _workflowResultDAO.getRootResultConditions(action.getConditionalResults(), _getStepId(nodeId)).get(0)
705                : action.getRestriction().getConditionsDescriptor();
706
707        int lastIndexOf = nodeId.lastIndexOf('-');
708        String parentNodeId = nodeId.substring(0, lastIndexOf);
709        ConditionsDescriptor parentNode = _getConditionsNode(rootCondition, parentNodeId);
710        int nodeToDeleteIndex = _getConditionIndex(path, path.length - 1);
711        List conditions = parentNode.getConditions();
712        ConditionDescriptor conditionToRemove = (ConditionDescriptor) conditions.get(nodeToDeleteIndex);
713        conditions.remove(conditionToRemove);
714        
715        if (conditions.isEmpty() && (path.length == 1 || path.length == 2 && path[0].startsWith(AND))) //if only an operator "and' is left
716        {
717            action.setRestriction(null);
718        }
719        
720        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
721        
722        return _getConditionProperties(workflowDescriptor, action, conditionToRemove, stepId, nodeId);
723    }
724    
725    /**
726     * Remove a condition
727     * @param rootCondition the first parent condition operator
728     * @param conditionNodeId id of the current condition
729     * @param conditionPath the full path to the condition
730     * @return the list of conditions after removal
731     */
732    protected List _removeCondition(ConditionsDescriptor rootCondition, String conditionNodeId, String[] conditionPath)
733    {
734        int lastIndexOf = conditionNodeId.lastIndexOf('-');
735        String parentNodeId = conditionNodeId.substring(0, lastIndexOf);
736        ConditionsDescriptor parentNode = _getConditionsNode(rootCondition, parentNodeId);
737        int nodeToDeleteIndex = _getConditionIndex(conditionPath, conditionPath.length - 1);
738        List conditions = parentNode.getConditions();
739        conditions.remove(nodeToDeleteIndex);
740        
741        return conditions;
742    }
743    
744    /**
745     * Get the tree's condition nodes
746     * @param currentNode id of the current node 
747     * @param workflowName unique name of current workflow 
748     * @param actionId id of current action
749     * @return a map of current node's children
750     */
751    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
752    public Map<String, Object> getConditionNodes(String currentNode, String workflowName, Integer actionId) 
753    {
754        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
755        List<Map<String, Object>> nodes = new ArrayList<>();
756        if (_workflowRightHelper.canRead(workflowDescriptor))
757        {
758            ActionDescriptor action = workflowDescriptor.getAction(actionId);
759            List<AbstractDescriptor> conditions = _getConditions(currentNode, action);
760            if (!conditions.isEmpty())
761            {
762                boolean rootIsAND = !action.getRestriction().getConditionsDescriptor().getType().equals(OR);
763                for (int i = 0; i < conditions.size(); i++)
764                {
765                    nodes.add(conditionToJSON(conditions.get(i), currentNode, i, rootIsAND));
766                }
767            }
768        }
769        
770        return Map.of("conditions", nodes);
771    }
772    
773    /**
774     * Get conditions below current node
775     * @param currentNode id of the current node
776     * @param action current action
777     * @return a list of childnodes condition
778     */
779    protected List<AbstractDescriptor> _getConditions(String currentNode, ActionDescriptor action)
780    {
781        RestrictionDescriptor restriction = action.getRestriction();
782        if (restriction != null)
783        {
784            ConditionsDescriptor rootConditionsDescriptor = restriction.getConditionsDescriptor();
785
786            String[] path = _workflowResultDAO.getPath(currentNode);
787            // The current node is root and it's a OR node, so display it ...
788            if ("root".equals(currentNode) && rootConditionsDescriptor.getType().equals(OR))
789            {
790                return List.of(rootConditionsDescriptor);
791            }
792            // ... the current node is a AND node, display child conditions ...
793            else if (path.length == 1)
794            {
795                return rootConditionsDescriptor.getConditions();
796            }
797            // ... the selected node is not a condition, so it has children
798            // we need to search the condition and get child condition of current node
799            else if (!path[path.length - 1].startsWith("condition")) 
800            {
801                List<AbstractDescriptor> conditions = rootConditionsDescriptor.getConditions();
802                // get conditions for current node
803                int i = 1;
804                do
805                {
806                    String currentOrAndConditionId = path[i];
807                    int currentOrAndConditionIndex = (currentOrAndConditionId.startsWith("and"))
808                            ? Integer.valueOf(currentOrAndConditionId.substring(3))
809                            : Integer.valueOf(currentOrAndConditionId.substring(2));
810                    
811                    ConditionsDescriptor currentOrAndCondition = (ConditionsDescriptor) conditions.get(currentOrAndConditionIndex);
812                    conditions = currentOrAndCondition.getConditions();
813                    i++;
814                }
815                while (i < path.length);
816
817                return conditions;
818            }
819        }
820        
821        return List.of();
822    }
823    
824    /**
825     * Get condition or condition types properties 
826     * @param condition current condition, can be ConditionsDescriptor or ConditionDescriptor 
827     * @param currentNodeId the id of the current node in the ConditionTreePanel
828     * @param index index of current condition in node's condition list
829     * @param rootIsAND true if root node is an "AND" condition (in which case it's hidden and has no id)
830     * @return a map of the condition infos
831     */
832    public Map<String, Object> conditionToJSON(AbstractDescriptor condition, String currentNodeId, int index, boolean rootIsAND) 
833    {
834        Map<String, Object> infosConditions = new HashMap<>();
835        boolean isResult = currentNodeId.startsWith(__STEP_RESULT_PREFIX);
836        boolean isRoot = __ROOT_RESULT_ID.equals(currentNodeId) 
837                || isResult && _workflowResultDAO.getPath(currentNodeId).length == 1;
838        String prefix = isResult && isRoot ? currentNodeId + "-" : "";
839        prefix += isRoot && rootIsAND ? __AND_ROOT_OPERATOR : isRoot ? __OR_ROOT_OPERATOR : currentNodeId;
840        // if it's a 'and' or a 'or'
841        if (condition instanceof ConditionsDescriptor operator)
842        {
843            String type = ((ConditionsDescriptor) condition).getType();
844            if (!type.equals(OR))
845            {
846                String id = prefix + "-and" + index;
847                infosConditions.put("id", id);
848                I18nizableText i18nLabel = new I18nizableText("plugin.workflow", __ANDI18N);
849                infosConditions.put("label", i18nLabel);
850            }
851            else
852            {
853                String id = isRoot && !rootIsAND ? prefix : prefix + "-or" + index;
854                infosConditions.put("id", id);
855                I18nizableText i18nLabel = new I18nizableText("plugin.workflow", __ORI18N);
856                infosConditions.put("label", i18nLabel);
857            }
858            infosConditions.put("hasChildren", !operator.getConditions().isEmpty());
859        }
860        else //it's a condition
861        {
862            String id = prefix + "-condition" + index;
863            infosConditions.put("id", id);
864            infosConditions.put("conditionId", ((ConditionDescriptor) condition).getArgs().get("id"));
865            infosConditions.put("label", _getConditionLabel((ConditionDescriptor) condition));
866            infosConditions.put("hasChildren", false);
867        }
868       
869        return infosConditions;
870    }
871    
872    /**
873     * Get current condition's label
874     * @param workflowName unique name of current workflow
875     * @param actionId id of current action
876     * @param nodeId id of selected node
877     * @return the label
878     */
879    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
880    public I18nizableText getConditionLabel(String workflowName, Integer actionId, String nodeId)
881    {
882        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
883        
884        // Check user right
885        _workflowRightHelper.checkReadRight(workflowDescriptor);
886        
887        ConditionDescriptor condition = _getCondition(workflowDescriptor, actionId, nodeId);
888        return _getConditionLabel(condition);
889    }
890    
891    /**
892     * Get if current operator has children conditions
893     * @param workflowName unique name of current workflow
894     * @param actionId id of current action
895     * @param nodeId id of selected operator node
896     * @return true if operator has children
897     */
898    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
899    public boolean hasChildConditions(String workflowName, Integer actionId, String nodeId)
900    { 
901        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
902        
903        // Check user right
904        _workflowRightHelper.checkReadRight(workflowDescriptor);
905        
906        ActionDescriptor action = workflowDescriptor.getAction(actionId);
907        List<AbstractDescriptor> conditions = nodeId.startsWith("step") 
908                ? _workflowResultDAO.getChildrenResultConditions(nodeId, action, action.getConditionalResults())
909                : _getConditions(nodeId, action);
910        return  !conditions.isEmpty();
911    }
912    
913    /**
914     * Get condition's description or id 
915     * @param condition the current condition
916     * @return the condition description if exist, or its id if not
917     */
918    protected I18nizableText _getConditionLabel(ConditionDescriptor condition) 
919    {
920        String id = (String) condition.getArgs().get("id");
921        TypeResolver typeResolver = new AvalonTypeResolver(_manager);
922        try
923        {
924            Condition function = typeResolver.getCondition(condition.getType(), condition.getArgs());
925            if (function instanceof EnhancedCondition enhancedCondition)
926            {
927                List<WorkflowArgument> arguments = enhancedCondition.getArguments();
928                Map<String, String> values = new HashMap<>();
929                for (WorkflowArgument arg : arguments)
930                {
931                    values.put(arg.getName(), (String) condition.getArgs().get(arg.getName()));
932                }
933                I18nizableText description = _getDescription(enhancedCondition, values);
934                
935                return description != null ? description : new I18nizableText(id);
936            }
937        }
938        catch (WorkflowException e)
939        {
940            getLogger().warn("An error occured while resolving condition with id {}", id, e);
941        }
942        return new I18nizableText(id);
943    }
944
945    private I18nizableText _getDescription(EnhancedCondition function, Map<String, String> values)
946    {
947        try
948        {
949            return function.getFullLabel(values);
950        }
951        catch (Exception e)
952        {
953            getLogger().warn("Can't get description for condition '{}': a parameter might be missing", function.getClass().getName(), e);
954            return null;
955        }
956    }
957}