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.CHECKED_BY_IMPLEMENTATION)
077    public Map<String, Object> getConditionalResultsToJson(String workflowName, Integer actionId, Integer currentStepId)
078    {
079        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
080        
081        // Check user right
082        _workflowRightHelper.checkReadRight(workflowDescriptor);
083        
084        ActionDescriptor action = workflowDescriptor.getAction(actionId);
085        List<Integer> usedSteps = new ArrayList<>();
086        usedSteps.add(action.getUnconditionalResult().getStep());
087        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
088        for (ConditionalResultDescriptor result : conditionalResults)
089        {
090            usedSteps.add(result.getStep());
091        }
092        @SuppressWarnings("unchecked")
093        List<Map<String, Object>> workflowStatesToJson = (List<Map<String, Object>>) _workflowStepDAO.getStatesToJson(workflowName, actionId, workflowDescriptor.getInitialAction(actionId) != null).get("data");
094        List<Map<String, Object>> states = new ArrayList<>();
095        for (int i = 0; i < workflowStatesToJson.size(); i++)
096        {
097            Map<String, Object> state = workflowStatesToJson.get(i);
098            Integer id = (Integer) state.get("id");
099            if (!usedSteps.contains(id))
100            {
101                states.add(state);
102            }
103        }
104        if (currentStepId != null)
105        {
106            Map<String, Object> stateInfos = new HashMap<>();
107            stateInfos.put("id", currentStepId);
108            boolean showId = currentStepId != -1;
109            stateInfos.put("label", _workflowStepDAO.getStepLabelAsString(workflowDescriptor, currentStepId, showId));
110            states.add(stateInfos);
111        }
112        
113        return Map.of("data", states);
114    }
115    
116    /**
117     * Add a conditional result to action
118     * @param workflowName unique name of current workflow
119     * @param stepId id of the parent step of the selected action
120     * @param actionId id of selected action
121     * @param resultId id of step to add as conditional result
122     * @return map of result's infos
123     */
124    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
125    public Map<String, Object> addConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultId)
126    {
127        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
128        
129        // Check user right
130        _workflowRightHelper.checkEditRight(workflowDescriptor);
131        
132        ActionDescriptor action = workflowDescriptor.getAction(actionId);
133        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
134        Optional<ConditionalResultDescriptor> sameFinalState = conditionalResults.stream().filter(cr -> cr.getStep() == resultId).findAny();
135        if (sameFinalState.isPresent())
136        {
137            return Map.of("message", "duplicate-state");
138        }
139        
140        DescriptorFactory factory = new DescriptorFactory();
141        ConditionalResultDescriptor resultDescriptor = factory.createConditionalResultDescriptor();
142        resultDescriptor.setStep(resultId);
143        conditionalResults.add(resultDescriptor);
144        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
145        
146        return _getResultProperties(workflowDescriptor, action, stepId, resultId);
147    }
148    
149    /**
150     * Edit a conditional result
151     * @param workflowName unique name of current workflow
152     * @param stepId id of the parent step of the selected action
153     * @param actionId id of selected action
154     * @param resultOldId former id of selected result
155     * @param resultNewId new id for selected result
156     * @return map of result's infos
157     */
158    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
159    public Map<String, Object> editConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultOldId, Integer resultNewId)
160    {
161        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
162        
163        // Check user right
164        _workflowRightHelper.checkEditRight(workflowDescriptor);
165        
166        ActionDescriptor action = workflowDescriptor.getAction(actionId);
167        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
168        int indexOf = getIndexOfStepResult(conditionalResults, resultOldId);
169        if (indexOf != -1)
170        {
171            conditionalResults.get(indexOf).setStep(resultNewId);
172        }
173        
174        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
175        
176        return _getResultProperties(workflowDescriptor, action, stepId, resultNewId);
177    }
178    
179    /**
180     * Delete a conditional result from action
181     * @param workflowName unique name of current workflow
182     * @param stepId id of the parent step of the selected action
183     * @param actionId id of selected action
184     * @param resultId id of the result to delete
185     * @return map of result's infos
186     */
187    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
188    public Map<String, Object> deleteConditionalResult(String workflowName, Integer stepId, Integer actionId, Integer resultId)
189    {
190        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
191        
192        // Check user right
193        _workflowRightHelper.checkEditRight(workflowDescriptor);
194        
195        ActionDescriptor action = workflowDescriptor.getAction(actionId);
196        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
197        int indexOf = getIndexOfStepResult(conditionalResults, resultId);
198        if (indexOf != -1)
199        {
200            conditionalResults.remove(indexOf);
201        }
202        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
203        
204        return _getResultProperties(workflowDescriptor, action, stepId, resultId);
205    }
206
207    private Map<String, Object> _getResultProperties(WorkflowDescriptor workflowDescriptor, ActionDescriptor action, Integer stepId, Integer resultId)
208    {
209        Map<String, Object> results = new HashMap<>();
210        results.put("nodeId", "step" + resultId);
211        results.put("nodeLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, resultId));
212        results.put("actionId", action.getId());
213        results.put("actionLabel", _workflowTransitionDAO.getActionLabel(action));
214        results.put("stepId", stepId);
215        results.put("stepLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, stepId));
216        results.put("workflowId", workflowDescriptor.getName());
217        results.put("isConditional", true);
218        return results;
219    }
220    
221    /**
222     * Get children conditions of current node in result tree
223     * @param currentNode id of current node in tree
224     * @param action the selected action
225     * @param conditionalResults the action's conditional results
226     * @return a list of conditions, can be empty
227     */
228    protected List<AbstractDescriptor> getChildrenResultConditions(String currentNode, ActionDescriptor action, List<ConditionalResultDescriptor> conditionalResults)
229    {
230        if (!conditionalResults.isEmpty())
231        {
232            String[] path = getPath(currentNode);
233            int stepId = Integer.valueOf(path[0].substring(4));
234            List<ConditionsDescriptor> resultConditions = getRootResultConditions(conditionalResults, stepId);
235            if (!resultConditions.isEmpty())
236            {
237                ConditionsDescriptor rootConditionsDescriptor = resultConditions.get(0);
238                
239                // The current node is root and it's a OR node, so display it ...
240                if (path.length == 1 && rootConditionsDescriptor.getType().equals(WorkflowConditionDAO.OR))
241                {
242                    return List.of(rootConditionsDescriptor);
243                }
244                // ... the current node is a AND node, display child conditions ...
245                else if (path.length == 1)
246                {
247                    return rootConditionsDescriptor.getConditions();
248                }
249                // ... the selected node is not a condition, so it has children
250                // we need to search the condition and get child condition of current node
251                else if (!path[path.length - 1].startsWith("condition")) 
252                {
253                    List<AbstractDescriptor> conditions = new ArrayList<>();
254                    conditions.add(rootConditionsDescriptor);
255                    if (!conditions.isEmpty())
256                    {
257                        // get conditions for current node
258                        int i = 1;
259                        do
260                        {
261                            String currentOrAndConditionId = path[i];
262                            int currentOrAndConditionIndex = (currentOrAndConditionId.startsWith("and"))
263                                    ? Integer.valueOf(currentOrAndConditionId.substring(3))
264                                    : Integer.valueOf(currentOrAndConditionId.substring(2));
265                            
266                            ConditionsDescriptor currentOrAndCondition = (ConditionsDescriptor) conditions.get(currentOrAndConditionIndex);
267                            conditions = currentOrAndCondition.getConditions();
268                            i++;
269                        }
270                        while (i < path.length);
271                        
272                        return conditions;
273                    }
274                }
275            }
276        }
277        
278        return List.of();
279    }
280
281    /**
282     * Get the path to the current node
283     * @param currentNodeId id of the current node, is like 'step1-(and|or)0-condition4'
284     * @return an array of the path (step1, and0, condition4)
285     */
286    public String[] getPath(String currentNodeId)
287    {
288        String[] path = currentNodeId.split("-");
289        if (path.length > 1 && path[1].equals("1"))
290        {
291            path[0] += "-1";
292            String[] copyArray = new String[path.length - 1]; 
293            copyArray[0] = path[0];
294            System.arraycopy(path, 2, copyArray, 1, path.length - 2); 
295            path = copyArray;
296        }
297        return path;
298    }
299    
300    /**
301     * Get the root ConditionsDescriptor having current step as result
302     * this is the conditions descriptor that decide whether children conditions are linked by an AND or an OR operator
303     * @param conditionalResults list of current action's conditional results
304     * @param stepId id of current targeted step
305     * @return a list of the root ConditionsDescriptor if exist
306     */
307    public List<ConditionsDescriptor> getRootResultConditions(List<ConditionalResultDescriptor> conditionalResults, int stepId)
308    {
309        return conditionalResults.stream()
310                    .filter(r -> r.getStep() == stepId)
311                    .map(r -> r.getConditions())
312                    .findFirst()
313                    .orElse(List.of());
314    }
315    
316    /**
317     * Get the index of selected step result in the list of conditional results
318     * @param conditionalResults a list of conditional results
319     * @param stepResultId id of the result to find
320     * @return the step result's index
321     */
322    public int getIndexOfStepResult(List<ConditionalResultDescriptor> conditionalResults, Integer stepResultId)
323    {
324        for (int i = 0; i < conditionalResults.size(); i++)
325        {
326            if (conditionalResults.get(i).getStep() == stepResultId)
327            {
328                return i;
329            }
330        }
331        return -1;
332    }
333}