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