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(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 != oldId)
370        {
371            if (_workflowHelper.getAllActions(workflowDescriptor).contains(newId))
372            {
373                
374                return Map.of("message", "duplicate-id");
375            }
376            if (action.isCommon())
377            {
378                //Don't change edit order below: first update steps then update action
379                workflowDescriptor.getCommonActions().remove(oldId, action);
380                List<StepDescriptor> steps = workflowDescriptor.getSteps();
381                for (StepDescriptor step: steps)
382                {
383                    if (step.getAction(oldId) != null)
384                    {
385                        List<Integer> commonActions = step.getCommonActions();
386                        commonActions.remove(commonActions.indexOf(oldId));
387                        commonActions.add(newId);
388                    }
389                }
390                action.setId(newId);
391                workflowDescriptor.getCommonActions().put(newId, action);
392            }
393            else
394            {
395                action.setId(newId);
396            }
397        }
398        
399        DescriptorFactory factory = new DescriptorFactory();
400        ResultDescriptor finalStep = factory.createResultDescriptor();
401        finalStep.setStep(finalStepId);
402        action.setUnconditionalResult(finalStep);
403        
404        _updateActionName(workflowName, action);
405        I18nizableText actionKey = getActionLabel(action);
406        _workflowSessionHelper.updateTranslations(workflowName, actionKey, labels);
407        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
408        
409        return _getActionProperties(workflowDescriptor, action, stepId);
410    }
411    
412    /**
413     * Remove transition from step
414     * @param workflowName the current workflow name
415     * @param parentStepId the parent step id to remove the transition from
416     * @param transitionId the id for the transition to remove
417     * @return empty map if successfull
418     */
419    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
420    public Map<String, Object> removeTransition(String workflowName, Integer parentStepId, Integer transitionId)
421    {
422        WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true);
423        _workflowRightHelper.checkEditRight(workflowDescriptor);
424        ActionDescriptor actionToRemove = null;
425        if (isInitialStep(parentStepId))
426        {
427            actionToRemove = workflowDescriptor.getInitialAction(transitionId);
428            _workflowSessionHelper.removeTranslation(workflowName, new I18nizableText("application", actionToRemove.getName()));
429            workflowDescriptor.getInitialActions().remove(actionToRemove);
430        }
431        else
432        {
433            StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId);
434            actionToRemove = workflowDescriptor.getAction(transitionId);
435            stepDescriptor.getActions().remove(actionToRemove);
436            if (actionToRemove.isCommon())
437            {
438                List<Integer> commonActions = stepDescriptor.getCommonActions();
439                commonActions.remove(commonActions.indexOf(transitionId));
440                _manageCommonAction(workflowDescriptor, actionToRemove);
441            }
442            else
443            {
444                I18nizableText actionKey = getActionLabel(actionToRemove);
445                _workflowSessionHelper.removeTranslation(workflowName, actionKey); //use actionKey instead of action.getName() in case plugin's name is in the name
446            }
447        }
448        _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
449        
450        return _getActionProperties(workflowDescriptor, actionToRemove, parentStepId);
451    }
452    
453    /**
454     * Verify that the action is still used by multiple steps. If not anymore, replace the action by a non common copy 
455     * @param workflowDescriptor the current workflow
456     * @param action the action removed
457     */
458    protected void _manageCommonAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor action)
459    {
460        Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions();
461        Integer id = action.getId();
462        if (commonActions.containsKey(id))
463        {
464            List<StepDescriptor> steps = workflowDescriptor.getSteps();
465            int numberOfUse = _getNumberOfUse(id, steps);
466            if (numberOfUse == 1)
467            {
468                //only way to unset common in action is to replace it with a new copy 
469                StepDescriptor parentStep = _getParentStep(steps, id).get();
470                List<Integer> stepCommonActions = parentStep.getCommonActions();
471                stepCommonActions.remove(stepCommonActions.indexOf(id));
472                parentStep.getActions().remove(action);
473                workflowDescriptor.getCommonActions().remove(id, action);
474                ActionDescriptor actionCopy =  _copyAction(action);
475                parentStep.getActions().add(actionCopy);
476                _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor);
477            }
478        }
479    }
480    
481    private Optional<StepDescriptor> _getParentStep(List<StepDescriptor> steps, int actionId)
482    {
483        return steps.stream().filter(s -> s.getAction(actionId) != null).findFirst();
484    }
485
486    private ActionDescriptor _copyAction(ActionDescriptor actionToCopy)
487    {
488        DescriptorFactory factory = new DescriptorFactory();
489        ActionDescriptor action = factory.createActionDescriptor();
490        action.setId(actionToCopy.getId());
491        action.setName(actionToCopy.getName());
492        action.setUnconditionalResult(actionToCopy.getUnconditionalResult());
493        action.getConditionalResults().addAll(actionToCopy.getConditionalResults());
494        action.setMetaAttributes(actionToCopy.getMetaAttributes());
495        action.setRestriction(actionToCopy.getRestriction());
496        action.getPreFunctions().addAll(actionToCopy.getPreFunctions());
497        action.getPostFunctions().addAll(actionToCopy.getPostFunctions());
498        
499        return action;
500    }
501    
502    /**
503     * Get the number of steps using the action
504     * @param workflowName the current workflow's name
505     * @param actionId the current action's id
506     * @return the number of steps using the action
507     */
508    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
509    public int getNumberOfUse(String workflowName, Integer actionId)
510    {
511        WorkflowDescriptor workflow = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false);
512        return _getNumberOfUse(actionId, workflow.getSteps());
513    }
514    
515    private int _getNumberOfUse(Integer actionId, List<StepDescriptor> steps)
516    {
517        int stepSize = steps.size();
518        int numberOfUse = 0;
519        int index = 0;
520        while (index < stepSize)
521        {
522            if (steps.get(index).getAction(actionId) != null)
523            {
524                numberOfUse++;
525            }
526            index++;
527        }
528        return numberOfUse;
529    }
530    
531    /**
532     * Get the translated action label
533     * @param workflowName the workflow's unique name
534     * @param action current action
535     * @return the action label 
536     */
537    public String getActionLabel(String workflowName, ActionDescriptor action)
538    {
539        I18nizableText label = getActionLabel(action);
540        return _i18nHelper.translateKey(workflowName, label, DEFAULT_ACTION_NAME);
541    }
542    
543    /**
544     * Get the action's icon path 
545     * @param workflowName name of current workflow
546     * @param action current action
547     * @return the icon's path
548     */
549    public String getActionIconPath(String workflowName, ActionDescriptor action)
550    {
551        I18nizableText label = getActionLabel(action);
552        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 
553        return _workflowHelper.getElementIconPath(label, __DEFAULT_ACTION_ICON_PATH);
554    }
555    
556    /**
557     * Get the action's icon path as base 64 for svg's links
558     * @param workflowName name of current workflow
559     * @param action current action
560     * @return the icon's path as base 64
561     */
562    public String getActionIconPathAsBase64(String workflowName, ActionDescriptor action) 
563    {
564        I18nizableText label = getActionLabel(action);
565        label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label);
566        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_ACTION_ICON_PATH);
567    }
568    
569    /**
570     * Get the action label as i18n
571     * @param action the current action
572     * @return the label as i18n
573     */
574    public I18nizableText getActionLabel(ActionDescriptor action)
575    {
576        return new I18nizableText("application", action.getName());
577    }
578}