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