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