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