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.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.Set;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.avalon.framework.service.Serviceable;
031
032import org.ametys.core.ui.Callable;
033import org.ametys.plugins.workflow.component.WorkflowLanguageManager;
034import org.ametys.plugins.workflow.support.I18nHelper;
035import org.ametys.plugins.workflow.support.WorflowRightHelper;
036import org.ametys.plugins.workflow.support.WorkflowHelper;
037import org.ametys.plugins.workflow.support.WorkflowSessionHelper;
038import org.ametys.runtime.i18n.I18nizableText;
039
040import com.opensymphony.workflow.loader.ActionDescriptor;
041import com.opensymphony.workflow.loader.DescriptorFactory;
042import com.opensymphony.workflow.loader.ResultDescriptor;
043import com.opensymphony.workflow.loader.StepDescriptor;
044import com.opensymphony.workflow.loader.WorkflowDescriptor;
045
046/**
047 * The workflow action DAO
048 */
049public class WorkflowTransitionDAO implements Component, Serviceable
050{
051    /** The component's role */
052    public static final String ROLE =  WorkflowTransitionDAO.class.getName();
053    
054    /** The default label for actions */
055    public static final I18nizableText DEFAULT_ACTION_NAME = new I18nizableText("plugin.workflow", "PLUGIN_WORKFLOW_DEFAULT_ACTION_LABEL");    
056    
057    /** Default path for svg action icons */
058    private static final String __DEFAULT_SVG_ACTION_ICON_PATH = "plugin:cms://resources/img/history/workflow/action_0_16.png";
059    
060    /** Default path for node action icons */
061    private static final String __DEFAULT_ACTION_ICON_PATH = "/plugins/cms/resources/img/history/workflow/action_0_16.png";
062    
063    /** The workflow session helper */
064    protected WorkflowSessionHelper _workflowSessionHelper;
065    
066    /** The workflow right helper */
067    protected WorflowRightHelper _workflowRightHelper;
068    
069    /** The Workflow Language Manager */
070    protected WorkflowLanguageManager _workflowLanguageManager;
071    
072    /** The workflow helper */
073    protected WorkflowHelper _workflowHelper;
074    
075    /** The workflow step DAO */
076    protected WorkflowStepDAO _workflowStepDAO;
077    
078    /** The helper for i18n translations and catalogs */
079    protected I18nHelper _i18nHelper;
080    
081    /** The context */
082    protected Context _context;
083    
084    public void service(ServiceManager smanager) throws ServiceException
085    {
086        _workflowSessionHelper = (WorkflowSessionHelper) smanager.lookup(WorkflowSessionHelper.ROLE);
087        _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE);
088        _workflowRightHelper = (WorflowRightHelper) smanager.lookup(WorflowRightHelper.ROLE);
089        _i18nHelper = (I18nHelper) smanager.lookup(I18nHelper.ROLE);
090        _workflowLanguageManager = (WorkflowLanguageManager) smanager.lookup(WorkflowLanguageManager.ROLE);
091        _workflowStepDAO = (WorkflowStepDAO) smanager.lookup(WorkflowStepDAO.ROLE);
092    }
093    
094    /**
095     * Get the transition infos to initialize creation/edition form fields
096     * @param workflowName the current workflow name 
097     * @param transitionId the current transition's id, can be null
098     * @return the transition values and a list of taken ids
099     */
100    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
101    public Map<String, Object> getTransitionInfos(String workflowName, Integer transitionId)
102    {
103        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
104        
105        // Check user right
106        _workflowRightHelper.checkEditRight(workflowDescriptor);
107        
108        Map<String, Object> transitionInfos = new HashMap<>();
109        Set<Integer> transitionIds = _workflowHelper.getAllActions(workflowDescriptor);
110        I18nizableText labelKey = DEFAULT_ACTION_NAME;
111        if (transitionId != null)
112        {
113            transitionIds.remove(transitionId);
114            transitionInfos.put("id", transitionId);
115            ActionDescriptor action = workflowDescriptor.getAction(transitionId);
116            labelKey = getActionLabel(action); 
117            
118            transitionInfos.put("finalStep", action.getUnconditionalResult().getStep());
119        }
120        transitionInfos.put("ids", transitionIds);
121        
122        Map<String, String> translations = _workflowSessionHelper.getTranslation(workflowName, labelKey);
123        if (translations == null)
124        {
125            translations = new HashMap<>();
126            translations.put(_workflowLanguageManager.getCurrentLanguage(), _i18nHelper.translateKey(workflowDescriptor.getName(), labelKey, DEFAULT_ACTION_NAME));
127        }
128        transitionInfos.put("labels", translations);
129        
130        return transitionInfos;
131    }
132    
133    /**
134     * Get a set of transitions already defined in current workflow beside initialActions
135     * @param workflowName the current workflow name 
136     * @param parentStepId the current selected step
137     * @return a set containing maps of actions properties
138     */
139    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
140    public Set<Map<String, Object>> getAvailableActions(String workflowName, Integer parentStepId)
141    {
142        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
143        
144        // Check user right
145        _workflowRightHelper.checkReadRight(workflowDescriptor);
146        
147        
148        Set<Map<String, Object>> availableActions = new HashSet<>();
149        if (0 != parentStepId) // generic actions can't be used as  initial actions
150        {
151            StepDescriptor parentStep = workflowDescriptor.getStep(parentStepId);
152            List<ActionDescriptor> actions = parentStep.getActions();
153            
154            List<StepDescriptor> steps = workflowDescriptor.getSteps();
155            for (StepDescriptor step: steps)
156            {
157                List<ActionDescriptor> outgoingActions = step.getActions();
158                for (ActionDescriptor outgoingAction: outgoingActions)
159                {
160                    if (!actions.contains(outgoingAction))
161                    {
162                        availableActions.add(_getActionInfos(workflowName, outgoingAction));
163                    }
164                }
165            }
166        }
167        return availableActions;
168    }
169
170    private Map<String, Object> _getActionInfos(String workflowName, ActionDescriptor action)
171    {
172        Map<String, Object> actionInfos = new HashMap<>();
173        int actionId = action.getId();
174        actionInfos.put("id", actionId);
175        actionInfos.put("label", getActionLabel(workflowName, action) + " (" + actionId + ")");
176        return actionInfos;
177    }
178    
179    /**
180     * Create a new transition
181     * @param workflowName the current workflow name
182     * @param parentStepId the parent step id
183     * @param transitionId the id for the transition to create
184     * @param labels the multilinguals labels for the transition
185     * @param finalStepId the id for the transition's unconditional result
186     * @return a map with error message or with transition's id if succesfull 
187     */
188    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
189    public Map<String, Object> createTransition(String workflowName, Integer parentStepId, int transitionId, Map<String, String> labels, int finalStepId)
190    {
191        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
192        
193        // Check user right
194        _workflowRightHelper.checkEditRight(workflowDescriptor);
195        
196        if (_workflowHelper.getAllActions(workflowDescriptor).contains(transitionId))
197        {
198            return Map.of("message", "duplicate-id");
199        }
200        
201        DescriptorFactory factory = new DescriptorFactory();
202        ActionDescriptor action = factory.createActionDescriptor();
203        action.setId(transitionId);
204        I18nizableText actionNameI18nKey = _i18nHelper.generateI18nKey(workflowName, "ACTION", transitionId);
205        action.setName(actionNameI18nKey.toString());
206        ResultDescriptor finalStep = factory.createResultDescriptor();
207        finalStep.setStep(finalStepId);
208        action.setUnconditionalResult(finalStep);
209        
210        if (isInitialStep(parentStepId))
211        {
212            workflowDescriptor.addInitialAction(action);
213        }
214        else
215        {
216            StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId);
217            stepDescriptor.getActions().add(action);
218        }
219        
220        _workflowSessionHelper.updateTranslations(workflowName, actionNameI18nKey, labels);
221        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
222        
223        return _getActionProperties(workflowDescriptor, action, parentStepId);
224    }
225
226    private Map<String, Object> _getActionProperties(WorkflowDescriptor workflowDescriptor, ActionDescriptor action, Integer stepId)
227    {
228        Map<String, Object> results = new HashMap<>();
229        results.put("actionId", action.getId());
230        results.put("actionLabels", getActionLabel(workflowDescriptor.getName(), action));
231        results.put("stepId", stepId);
232        results.put("stepLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, stepId));
233        results.put("workflowId", workflowDescriptor.getName());
234        
235        return results;
236    }
237
238    private boolean isInitialStep(Integer stepIdToInt)
239    {
240        return stepIdToInt == 0;
241    }
242    
243    /**
244     * Add an existing transition to current step
245     * @param workflowName the current workflow name
246     * @param parentStepId the current selected step id
247     * @param transitionIds a list of ids corresponding to the transitions to add to current step
248     * @return the first transition id and its parent sted id
249     */
250    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
251    public Map<String, Object> addTransitions(String workflowName, Integer parentStepId, List<Integer> transitionIds)
252    {
253        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
254        
255        // Check user right
256        _workflowRightHelper.checkEditRight(workflowDescriptor);
257        
258        Map<String, Object> results = new HashMap<>();
259        
260        if (parentStepId != 0)
261        {
262            StepDescriptor step = workflowDescriptor.getStep(parentStepId);
263            for (Integer id : transitionIds)
264            {
265                ActionDescriptor action = _getAction(workflowDescriptor, id);
266                step.getActions().add(action);
267                if (!action.isCommon())
268                {
269                    _updateWorkflowCommonAction(workflowDescriptor, id, action);
270                }
271                else
272                {
273                    step.getCommonActions().add(id);
274                }
275            }
276            _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
277            results = _getActionProperties(workflowDescriptor, workflowDescriptor.getAction(transitionIds.get(0)), parentStepId);
278        }
279        else
280        {
281            results.put("message", "initial-step");
282        }
283        
284        return results;
285    }
286    
287    private ActionDescriptor _getAction(WorkflowDescriptor workflowDescriptor, Integer actionId)
288    {
289        ActionDescriptor action = workflowDescriptor.getAction(actionId);
290        if (action == null)
291        {
292            Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions();
293            action = commonActions.get(actionId);
294        }
295        return action;
296    }
297    
298    /**
299     * Set action as 'common' in steps using it
300     * @param workflowDescriptor the current workflow
301     * @param actionId id of current action
302     * @param action the action
303     */
304    protected void _updateWorkflowCommonAction(WorkflowDescriptor workflowDescriptor, int actionId, ActionDescriptor action)
305    {
306        List<StepDescriptor> stepsToUpdate = new ArrayList<>();
307        List<StepDescriptor> steps = workflowDescriptor.getSteps();
308        
309        //remove individual action from steps 
310        for (StepDescriptor step : steps)
311        {
312            if (step.getAction(actionId) != null)
313            {
314                step.getActions().remove(action);
315                stepsToUpdate.add(step);
316            }
317        }
318        //set action as common in workflow
319        workflowDescriptor.addCommonAction(action);
320        
321        //put back action in steps as common action
322        for (StepDescriptor step : stepsToUpdate)
323        {
324            step.getCommonActions().add(actionId);
325            step.getActions().add(action);
326        }
327    }
328    
329    /**
330     * Rename the transition
331     * @param workflowName the workflow name
332     * @param stepId id of selected step
333     * @param actionId the transition's id
334     * @param newMainLabel the new label in current language
335     * @return  a map with error message or with transition's infos if succesfull 
336     */
337    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
338    public Map<String, Object> editTransitionLabel(String workflowName, Integer stepId, Integer actionId, String newMainLabel)
339    {
340        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
341        
342        // Check user right
343        _workflowRightHelper.checkEditRight(workflowDescriptor);
344        
345        ActionDescriptor action = workflowDescriptor.getAction(actionId);
346        _updateActionName(workflowName, action);
347        I18nizableText actionKey = getActionLabel(action);
348        _workflowSessionHelper.updateTranslations(workflowName, actionKey, Map.of(_workflowLanguageManager.getCurrentLanguage(), newMainLabel));
349        
350        return _getActionProperties(workflowDescriptor, action, stepId);
351    }
352
353    private void _updateActionName(String workflowName, ActionDescriptor action)
354    {
355        String defaultCatalog = _workflowHelper.getWorkflowCatalog(workflowName);
356        I18nizableText actionKey = getActionLabel(action);
357        
358        if (!defaultCatalog.equals(actionKey.getCatalogue()))
359        {
360            String newName = new I18nizableText(defaultCatalog, actionKey.getKey()).toString();
361            action.setName(newName);
362        }
363    }
364    
365    /**
366     * Edit the transition
367     * @param workflowName the workflow name
368     * @param stepId id of selected step
369     * @param oldId the transition's former id
370     * @param newId the transition's new id
371     * @param labels the transition's multilingual labels
372     * @param finalStepId the transition's unconditional result id
373     * @return a map with error message or with transition's infos if succesfull 
374     */
375    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
376    public Map<String, Object> editTransition(String workflowName, Integer stepId, Integer oldId, Integer newId, Map<String, String> labels, Integer finalStepId)
377    {
378        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
379        
380        // Check user right
381        _workflowRightHelper.checkEditRight(workflowDescriptor);
382        
383        ActionDescriptor action = workflowDescriptor.getAction(oldId);
384        
385        if (!newId.equals(oldId))
386        {
387            if (_workflowHelper.getAllActions(workflowDescriptor).contains(newId))
388            {
389                return Map.of("message", "duplicate-id");
390            }
391            if (action.isCommon())
392            {
393                //Don't change edit order below: first update steps then update action
394                workflowDescriptor.getCommonActions().remove(oldId, action);
395                List<StepDescriptor> steps = workflowDescriptor.getSteps();
396                for (StepDescriptor step: steps)
397                {
398                    if (step.getAction(oldId) != null)
399                    {
400                        List<Integer> commonActions = step.getCommonActions();
401                        commonActions.remove(commonActions.indexOf(oldId));
402                        commonActions.add(newId);
403                    }
404                }
405                action.setId(newId);
406                workflowDescriptor.getCommonActions().put(newId, action);
407            }
408            else
409            {
410                action.setId(newId);
411            }
412        }
413        
414        DescriptorFactory factory = new DescriptorFactory();
415        ResultDescriptor finalStep = factory.createResultDescriptor();
416        finalStep.setStep(finalStepId);
417        action.setUnconditionalResult(finalStep);
418        
419        _updateActionName(workflowName, action);
420        I18nizableText actionKey = getActionLabel(action);
421        _workflowSessionHelper.updateTranslations(workflowName, actionKey, labels);
422        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
423        
424        return _getActionProperties(workflowDescriptor, action, stepId);
425    }
426    
427    /**
428     * Remove transition from step
429     * @param workflowName the current workflow name
430     * @param parentStepId the parent step id to remove the transition from
431     * @param transitionId the id for the transition to remove
432     * @return empty map if successfull
433     */
434    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
435    public Map<String, Object> removeTransition(String workflowName, Integer parentStepId, Integer transitionId)
436    {
437        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
438        
439        // Check user right
440        _workflowRightHelper.checkEditRight(workflowDescriptor);
441        
442        ActionDescriptor actionToRemove = null;
443        if (isInitialStep(parentStepId))
444        {
445            actionToRemove = workflowDescriptor.getInitialAction(transitionId);
446            _workflowSessionHelper.removeTranslation(workflowName, new I18nizableText("application", actionToRemove.getName()));
447            workflowDescriptor.getInitialActions().remove(actionToRemove);
448        }
449        else
450        {
451            StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId);
452            actionToRemove = workflowDescriptor.getAction(transitionId);
453            stepDescriptor.getActions().remove(actionToRemove);
454            if (actionToRemove.isCommon())
455            {
456                List<Integer> commonActions = stepDescriptor.getCommonActions();
457                commonActions.remove(commonActions.indexOf(transitionId));
458                _manageCommonAction(workflowDescriptor, actionToRemove);
459            }
460            else
461            {
462                I18nizableText actionKey = getActionLabel(actionToRemove);
463                _workflowSessionHelper.removeTranslation(workflowName, actionKey); //use actionKey instead of action.getName() in case plugin's name is in the name
464            }
465        }
466        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
467        
468        return _getActionProperties(workflowDescriptor, actionToRemove, parentStepId);
469    }
470    
471    /**
472     * Verify that the action is still used by multiple steps. If not anymore, replace the action by a non common copy 
473     * @param workflowDescriptor the current workflow
474     * @param action the action removed
475     */
476    protected void _manageCommonAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor action)
477    {
478        Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions();
479        Integer id = action.getId();
480        if (commonActions.containsKey(id))
481        {
482            List<StepDescriptor> steps = workflowDescriptor.getSteps();
483            int numberOfUse = _getNumberOfUse(id, steps);
484            if (numberOfUse == 1)
485            {
486                //only way to unset common in action is to replace it with a new copy 
487                StepDescriptor parentStep = _getParentStep(steps, id).get();
488                List<Integer> stepCommonActions = parentStep.getCommonActions();
489                stepCommonActions.remove(stepCommonActions.indexOf(id));
490                parentStep.getActions().remove(action);
491                workflowDescriptor.getCommonActions().remove(id, action);
492                ActionDescriptor actionCopy =  _copyAction(action);
493                parentStep.getActions().add(actionCopy);
494                _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
495            }
496        }
497    }
498    
499    private Optional<StepDescriptor> _getParentStep(List<StepDescriptor> steps, int actionId)
500    {
501        return steps.stream().filter(s -> s.getAction(actionId) != null).findFirst();
502    }
503
504    private ActionDescriptor _copyAction(ActionDescriptor actionToCopy)
505    {
506        DescriptorFactory factory = new DescriptorFactory();
507        ActionDescriptor action = factory.createActionDescriptor();
508        action.setId(actionToCopy.getId());
509        action.setName(actionToCopy.getName());
510        action.setUnconditionalResult(actionToCopy.getUnconditionalResult());
511        action.getConditionalResults().addAll(actionToCopy.getConditionalResults());
512        action.setMetaAttributes(actionToCopy.getMetaAttributes());
513        action.setRestriction(actionToCopy.getRestriction());
514        action.getPreFunctions().addAll(actionToCopy.getPreFunctions());
515        action.getPostFunctions().addAll(actionToCopy.getPostFunctions());
516        
517        return action;
518    }
519    
520    /**
521     * Get the number of steps using the action
522     * @param workflowName the current workflow's name
523     * @param actionId the current action's id
524     * @return the number of steps using the action
525     */
526    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
527    public int getNumberOfUse(String workflowName, Integer actionId)
528    {
529        WorkflowDescriptor workflow = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
530        
531        _workflowRightHelper.checkReadRight(workflow);
532        
533        return _getNumberOfUse(actionId, workflow.getSteps());
534    }
535    
536    private int _getNumberOfUse(Integer actionId, List<StepDescriptor> steps)
537    {
538        int stepSize = steps.size();
539        int numberOfUse = 0;
540        int index = 0;
541        while (index < stepSize)
542        {
543            if (steps.get(index).getAction(actionId) != null)
544            {
545                numberOfUse++;
546            }
547            index++;
548        }
549        return numberOfUse;
550    }
551    
552    /**
553     * Get the translated action label
554     * @param workflowName the workflow's unique name
555     * @param action current action
556     * @return the action label 
557     */
558    public String getActionLabel(String workflowName, ActionDescriptor action)
559    {
560        I18nizableText label = getActionLabel(action);
561        return _i18nHelper.translateKey(workflowName, label, DEFAULT_ACTION_NAME);
562    }
563    
564    /**
565     * Get the action's icon path 
566     * @param workflowName name of current workflow
567     * @param action current action
568     * @return the icon's path
569     */
570    public String getActionIconPath(String workflowName, ActionDescriptor action)
571    {
572        I18nizableText label = getActionLabel(action);
573        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 
574        return _workflowHelper.getElementIconPath(label, __DEFAULT_ACTION_ICON_PATH);
575    }
576    
577    /**
578     * Get the action's icon path as base 64 for svg's links
579     * @param workflowName name of current workflow
580     * @param action current action
581     * @return the icon's path as base 64
582     */
583    public String getActionIconPathAsBase64(String workflowName, ActionDescriptor action) 
584    {
585        I18nizableText label = getActionLabel(action);
586        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label);
587        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_ACTION_ICON_PATH);
588    }
589    
590    /**
591     * Get the action label as i18n
592     * @param action the current action
593     * @return the label as i18n
594     */
595    public I18nizableText getActionLabel(ActionDescriptor action)
596    {
597        return new I18nizableText("application", action.getName());
598    }
599}