001/*
002 *  Copyright 2023 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workflow.dao;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Optional;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.avalon.framework.service.Serviceable;
028
029import org.ametys.core.ui.Callable;
030import org.ametys.plugins.workflow.support.WorflowRightHelper;
031import org.ametys.plugins.workflow.support.WorkflowSessionHelper;
032import org.ametys.runtime.plugin.component.AbstractLogEnabled;
033
034import com.opensymphony.workflow.loader.AbstractDescriptor;
035import com.opensymphony.workflow.loader.ActionDescriptor;
036import com.opensymphony.workflow.loader.ConditionalResultDescriptor;
037import com.opensymphony.workflow.loader.ConditionsDescriptor;
038import com.opensymphony.workflow.loader.DescriptorFactory;
039import com.opensymphony.workflow.loader.WorkflowDescriptor;
040
041/**
042 * DAO for workflow results
043 */
044public class WorkflowResultDAO extends AbstractLogEnabled implements Component, Serviceable
045{
046    /** The component role */ 
047    public static final String ROLE = WorkflowResultDAO.class.getName();
048    
049    /** The workflow session helper */
050    protected WorkflowSessionHelper _workflowSessionHelper;
051    
052    /** The workflow right helper */
053    protected WorflowRightHelper _workflowRightHelper;
054    
055    /** The workflow step DAO */
056    protected WorkflowStepDAO _workflowStepDAO;
057    
058    /** The workflow transition DAO */
059    protected WorkflowTransitionDAO _workflowTransitionDAO;
060    
061    public void service(ServiceManager manager) throws ServiceException
062    {
063        _workflowSessionHelper = (WorkflowSessionHelper) manager.lookup(WorkflowSessionHelper.ROLE);
064        _workflowRightHelper = (WorflowRightHelper) manager.lookup(WorflowRightHelper.ROLE);
065        _workflowStepDAO = (WorkflowStepDAO) manager.lookup(WorkflowStepDAO.ROLE);
066        _workflowTransitionDAO = (WorkflowTransitionDAO) manager.lookup(WorkflowTransitionDAO.ROLE);
067    }
068    
069    /**
070     * Get a list of the action's conditional results in a json format 
071     * @param workflowName  the current workflow's  unique name
072     * @param actionId  the current action
073     * @param currentStepId in edition mode, id of the selected final step 
074     * @return a list of available conditional results for this action
075     */
076    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
077    public Map<String, Object> getConditionalResultsToJson(String workflowName, Integer actionId, Integer currentStepId)
078    {
079        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
080        _workflowRightHelper.checkReadRight(workflowDescriptor);
081        ActionDescriptor action = workflowDescriptor.getAction(actionId);
082        List<Integer> usedSteps = new ArrayList<>();
083        usedSteps.add(action.getUnconditionalResult().getStep());
084        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
085        for (ConditionalResultDescriptor result : conditionalResults)
086        {
087            usedSteps.add(result.getStep());
088        }
089        @SuppressWarnings("unchecked")
090        List<Map<String, Object>> workflowStatesToJson = (List<Map<String, Object>>) _workflowStepDAO.getStatesToJson(workflowName, actionId, workflowDescriptor.getInitialAction(actionId) != null).get("data");
091        List<Map<String, Object>> states = new ArrayList<>();
092        for (int i = 0; i < workflowStatesToJson.size(); i++)
093        {
094            Map<String, Object> state = workflowStatesToJson.get(i);
095            Integer id = (Integer) state.get("id");
096            if (!usedSteps.contains(id))
097            {
098                states.add(state);
099            }
100        }
101        if (currentStepId != null)
102        {
103            Map<String, Object> stateInfos = new HashMap<>();
104            stateInfos.put("id", currentStepId);
105            boolean showId = currentStepId != -1;
106            stateInfos.put("label", _workflowStepDAO.getStepLabelAsString(workflowDescriptor, currentStepId, showId));
107            states.add(stateInfos);
108        }
109        
110        return Map.of("data", states);
111    }
112    
113    /**
114     * Add a conditional result to action
115     * @param workflowName unique name of current workflow
116     * @param stepId id of the parent step of the selected action
117     * @param actionId id of selected action
118     * @param resultId id of step to add as conditional result
119     * @return map of result's infos
120     */
121    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
122    public Map<String, Object> addConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultId)
123    {
124        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
125        _workflowRightHelper.checkEditRight(workflowDescriptor);
126        ActionDescriptor action = workflowDescriptor.getAction(actionId);
127        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
128        Optional<ConditionalResultDescriptor> sameFinalState = conditionalResults.stream().filter(cr -> cr.getStep() == resultId).findAny();
129        if (sameFinalState.isPresent())
130        {
131            return Map.of("message", "duplicate-state");
132        }
133        
134        DescriptorFactory factory = new DescriptorFactory();
135        ConditionalResultDescriptor resultDescriptor = factory.createConditionalResultDescriptor();
136        resultDescriptor.setStep(resultId);
137        conditionalResults.add(resultDescriptor);
138        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
139        
140        return _getResultProperties(workflowDescriptor, action, stepId, resultId);
141    }
142    
143    /**
144     * Edit a conditional result
145     * @param workflowName unique name of current workflow
146     * @param stepId id of the parent step of the selected action
147     * @param actionId id of selected action
148     * @param resultOldId former id of selected result
149     * @param resultNewId new id for selected result
150     * @return map of result's infos
151     */
152    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
153    public Map<String, Object> editConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultOldId, Integer resultNewId)
154    {
155        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
156        _workflowRightHelper.checkEditRight(workflowDescriptor);
157        ActionDescriptor action = workflowDescriptor.getAction(actionId);
158        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
159        int indexOf = getIndexOfStepResult(conditionalResults, resultOldId);
160        if (indexOf != -1)
161        {
162            conditionalResults.get(indexOf).setStep(resultNewId);
163        }
164        
165        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
166        
167        return _getResultProperties(workflowDescriptor, action, stepId, resultNewId);
168    }
169    
170    /**
171     * Delete a conditional result from action
172     * @param workflowName unique name of current workflow
173     * @param stepId id of the parent step of the selected action
174     * @param actionId id of selected action
175     * @param resultId id of the result to delete
176     * @return map of result's infos
177     */
178    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
179    public Map<String, Object> deleteConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultId)
180    {
181        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
182        _workflowRightHelper.checkEditRight(workflowDescriptor);
183        ActionDescriptor action = workflowDescriptor.getAction(actionId);
184        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
185        int indexOf = getIndexOfStepResult(conditionalResults, resultId);
186        if (indexOf != -1)
187        {
188            conditionalResults.remove(indexOf);
189        }
190        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
191        
192        return _getResultProperties(workflowDescriptor, action, stepId, resultId);
193    }
194
195    private Map<String, Object> _getResultProperties(WorkflowDescriptor workflowDescriptor, ActionDescriptor action, Integer stepId, Integer resultId)
196    {
197        Map<String, Object> results = new HashMap<>();
198        results.put("nodeId", "step" + resultId);
199        results.put("nodeLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, resultId));
200        results.put("actionId", action.getId());
201        results.put("actionLabel", _workflowTransitionDAO.getActionLabel(action));
202        results.put("stepId", stepId);
203        results.put("stepLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, stepId));
204        results.put("workflowId", workflowDescriptor.getName());
205        results.put("isConditional", true);
206        return results;
207    }
208    
209    /**
210     * Get children conditions of current node in result tree
211     * @param currentNode id of current node in tree
212     * @param action the selected action
213     * @param conditionalResults the action's conditional results
214     * @return a list of conditions, can be empty
215     */
216    protected List<AbstractDescriptor> getChildrenResultConditions(String currentNode, ActionDescriptor action, List<ConditionalResultDescriptor> conditionalResults)
217    {
218        if (!conditionalResults.isEmpty())
219        {
220            String[] path = getPath(currentNode);
221            int stepId = Integer.valueOf(path[0].substring(4));
222            List<ConditionsDescriptor> resultConditions = getRootResultConditions(conditionalResults, stepId);
223            if (!resultConditions.isEmpty())
224            {
225                ConditionsDescriptor rootConditionsDescriptor = resultConditions.get(0);
226                
227                // The current node is root and it's a OR node, so display it ...
228                if (path.length == 1 && rootConditionsDescriptor.getType().equals(WorkflowConditionDAO.OR))
229                {
230                    return List.of(rootConditionsDescriptor);
231                }
232                // ... the current node is a AND node, display child conditions ...
233                else if (path.length == 1)
234                {
235                    return rootConditionsDescriptor.getConditions();
236                }
237                // ... the selected node is not a condition, so it has children
238                // we need to search the condition and get child condition of current node
239                else if (!path[path.length - 1].startsWith("condition")) 
240                {
241                    List<AbstractDescriptor> conditions = new ArrayList<>();
242                    conditions.add(rootConditionsDescriptor);
243                    if (!conditions.isEmpty())
244                    {
245                        // get conditions for current node
246                        int i = 1;
247                        do
248                        {
249                            String currentOrAndConditionId = path[i];
250                            int currentOrAndConditionIndex = (currentOrAndConditionId.startsWith("and"))
251                                    ? Integer.valueOf(currentOrAndConditionId.substring(3))
252                                    : Integer.valueOf(currentOrAndConditionId.substring(2));
253                            
254                            ConditionsDescriptor currentOrAndCondition = (ConditionsDescriptor) conditions.get(currentOrAndConditionIndex);
255                            conditions = currentOrAndCondition.getConditions();
256                            i++;
257                        }
258                        while (i < path.length);
259                        
260                        return conditions;
261                    }
262                }
263            }
264        }
265        
266        return List.of();
267    }
268
269    /**
270     * Get the path to the current node
271     * @param currentNodeId id of the current node, is like 'step1-(and|or)0-condition4'
272     * @return an array of the path (step1, and0, condition4)
273     */
274    public String[] getPath(String currentNodeId)
275    {
276        String[] path = currentNodeId.split("-");
277        if (path.length > 1 && path[1].equals("1"))
278        {
279            path[0] += "-1";
280            String[] copyArray = new String[path.length - 1]; 
281            copyArray[0] = path[0];
282            System.arraycopy(path, 2, copyArray, 1, path.length - 2); 
283            path = copyArray;
284        }
285        return path;
286    }
287    
288    /**
289     * Get the root ConditionsDescriptor having current step as result
290     * this is the conditions descriptor that decide whether children conditions are linked by an AND or an OR operator
291     * @param conditionalResults list of current action's conditional results
292     * @param stepId id of current targeted step
293     * @return a list of the root ConditionsDescriptor if exist
294     */
295    public List<ConditionsDescriptor> getRootResultConditions(List<ConditionalResultDescriptor> conditionalResults, int stepId)
296    {
297        return conditionalResults.stream()
298                    .filter(r -> r.getStep() == stepId)
299                    .map(r -> r.getConditions())
300                    .findFirst()
301                    .orElse(List.of());
302    }
303    
304    /**
305     * Get the index of selected step result in the list of conditional results
306     * @param conditionalResults a list of conditional results
307     * @param stepResultId id of the result to find
308     * @return the step result's index
309     */
310    public int getIndexOfStepResult(List<ConditionalResultDescriptor> conditionalResults, Integer stepResultId)
311    {
312        for (int i = 0; i < conditionalResults.size(); i++)
313        {
314            if (conditionalResults.get(i).getStep() == stepResultId)
315            {
316                return i;
317            }
318        }
319        return -1;
320    }
321}