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.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.jsoup.internal.StringUtil;
031
032import org.ametys.core.ui.Callable;
033import org.ametys.core.util.I18nUtils;
034import org.ametys.plugins.workflow.support.WorkflowHelper;
035import org.ametys.runtime.i18n.I18nizableText;
036import org.ametys.runtime.plugin.component.AbstractLogEnabled;
037
038import com.opensymphony.workflow.loader.AbstractDescriptor;
039import com.opensymphony.workflow.loader.ActionDescriptor;
040import com.opensymphony.workflow.loader.ConditionalResultDescriptor;
041import com.opensymphony.workflow.loader.ConditionsDescriptor;
042import com.opensymphony.workflow.loader.ResultDescriptor;
043import com.opensymphony.workflow.loader.StepDescriptor;
044import com.opensymphony.workflow.loader.WorkflowDescriptor;
045
046/**
047 * DAO for workflow steps 
048 */
049public class WorkflowStepDAO extends AbstractLogEnabled implements Component, Serviceable
050{
051    /** The component's role */
052    public static final String ROLE =  WorkflowStepDAO.class.getName();
053    
054    /** Id for initial step */
055    public static final String INITIAL_STEP_ID = "step0";
056    
057    /** The default label for steps */
058    public static final I18nizableText DEFAULT_STEP_NAME = new I18nizableText("plugin.workflow", "PLUGIN_WORKFLOW_DEFAULT_STEP_LABEL");
059    
060    /** The default label for actions */
061    public static final I18nizableText DEFAULT_ACTION_NAME = new I18nizableText("plugin.workflow", "PLUGIN_WORKFLOW_DEFAULT_ACTION_LABEL");    
062    
063    /** Default path for svg step icons */
064    protected static final String __DEFAULT_SVG_STEP_ICON_PATH = "plugin:cms://resources/img/history/workflow/step_0_16.png";
065    /** Default path for svg action icons */
066    protected static final String __DEFAULT_SVG_ACTION_ICON_PATH = "plugin:cms://resources/img/history/workflow/action_0_16.png";
067    
068    private static final String __DEFAULT_STEP_ICON_PATH = "/plugins/cms/resources/img/history/workflow/step_0_16.png";
069    private static final String __DEFAULT_ACTION_ICON_PATH = "/plugins/cms/resources/img/history/workflow/action_0_16.png";
070    
071    /** The workflow helper */
072    protected WorkflowHelper _workflowHelper;
073    
074    /** The workflow condition DAO */
075    protected WorkflowConditionDAO _workflowConditionDAO;
076    
077    /** I18n Utils */
078    protected I18nUtils _i18nUtils;
079    
080    public void service(ServiceManager smanager) throws ServiceException
081    {
082        _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE);
083        _workflowConditionDAO = (WorkflowConditionDAO) smanager.lookup(WorkflowConditionDAO.ROLE);
084        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
085    }
086    
087    /**
088     * Get the workflow editor tree's nodes
089     * @param currentNode id of the current node
090     * @param workflowName unique name of current workflow
091     * @return a map of the current node's children
092     */
093    @Callable(right = "Workflow_Right_Read")
094    public Map<String, Object> getStepNodes(String currentNode, String workflowName) 
095    {
096        List<Map<String, Object>> nodes = new ArrayList<>();
097        WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName);
098        
099        List<ActionDescriptor> initialActions = workflowDescriptor.getInitialActions();
100        if (currentNode.equals("root"))
101        {
102            //initial step
103            Map<String, Object> infosInitialStep = _step2JSON(workflowDescriptor, 0, initialActions.size() > 0);
104            nodes.add(infosInitialStep);
105
106            //other steps
107            @SuppressWarnings("cast")
108            List<StepDescriptor> steps = (List<StepDescriptor>) workflowDescriptor.getSteps();
109            for (StepDescriptor step : steps)
110            {
111                Map<String, Object> infos = _step2JSON(workflowDescriptor, step.getId(), step.getActions().size() > 0);
112                nodes.add(infos);
113            }
114        }
115        else if (currentNode.equals(INITIAL_STEP_ID))
116        {
117            //initial actions
118            for (ActionDescriptor initialAction : initialActions)
119            {
120                nodes.add(_action2JSON(currentNode, initialAction, workflowDescriptor));
121            }
122        }
123        else
124        {
125            //regular actions
126            StepDescriptor step = workflowDescriptor.getStep(Integer.valueOf(currentNode.substring("step".length())));
127            for (ActionDescriptor transition : (List<ActionDescriptor>) step.getActions())
128            {
129                nodes.add(_action2JSON(currentNode, transition, workflowDescriptor));
130            }
131        }
132        
133        return Map.of("steps", nodes);
134    }
135    
136    /**
137     * Get the action infos for tree panel node
138     * @param stepId id of current step
139     * @param action currently processed action
140     * @param workflowDescriptor current workflow
141     * @return map of the action infos
142     */
143    protected Map<String, Object> _action2JSON(String stepId, ActionDescriptor action, WorkflowDescriptor workflowDescriptor)
144    {
145        Set<StepWithIcon> finalSteps = _getActionFinalSteps(action, workflowDescriptor);
146        Map<Integer, Object> finalStepNames = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.label()));
147        Map<Integer, Object> finalStepIcons = finalSteps.stream().collect(Collectors.toMap(s -> s.id(), s -> s.iconPath()));
148        
149        I18nizableText workflowActionName = new I18nizableText("application", action.getName());
150        String iconPath = _workflowHelper.getElementIconPath(workflowActionName, __DEFAULT_ACTION_ICON_PATH);
151        
152        Map<String, Object> infos = new HashMap<>();
153        
154        infos.put("id", stepId + "-action" + action.getId());
155        infos.put("elementId", action.getId());
156        infos.put("smallIcon", iconPath);
157        infos.put("label", getActionLabel(action));
158        infos.put("elementType", "action");
159        infos.put("hasChildren", false);
160        infos.put("targetedStepNames", finalStepNames);
161        infos.put("targetedStepIcons", finalStepIcons);
162        
163        return infos;
164    }
165    
166    /**
167     * Get the conditional and unconditional results of current action 
168     * @param action the current action
169     * @param workflowDescriptor the current workflow
170     * @return a list of the final steps as (stepId, stepLabel, StepIconPath)
171     */
172    protected Set<StepWithIcon> _getActionFinalSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor) 
173    {
174        Set<StepWithIcon> steps = new HashSet<>();
175        for (StepDescriptor step : getOutgoingSteps(action, workflowDescriptor))
176        {
177            int stepId = step.getId();
178            steps.add(new StepWithIcon(
179                stepId, 
180                getStepLabel(workflowDescriptor, stepId), 
181                getStepIconPath(workflowDescriptor, stepId))
182            );
183        }
184        
185        return steps;
186    }
187    
188    /**
189     * Get possible outgoing steps for action
190     * @param action the current action
191     * @param workflowDescriptor the current workflow
192     * @return a set of the outgoing steps
193     */
194    public Set<StepDescriptor> getOutgoingSteps(ActionDescriptor action, WorkflowDescriptor workflowDescriptor)
195    {
196        Set<StepDescriptor> outgoingSteps = new HashSet<>();
197        ResultDescriptor unconditionalResult = action.getUnconditionalResult();
198        if (unconditionalResult.getStep() != -1)
199        {
200            StepDescriptor unconditionalStep = workflowDescriptor.getStep(unconditionalResult.getStep());
201            outgoingSteps.add(unconditionalStep);
202        }
203        else
204        {
205            outgoingSteps.addAll(getIncomingSteps(action.getId(), workflowDescriptor));
206        }
207        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
208        for (ConditionalResultDescriptor result : conditionalResults)
209        {
210            StepDescriptor conditionalStep = workflowDescriptor.getStep(result.getStep());
211            outgoingSteps.add(conditionalStep);
212        }
213        
214        return outgoingSteps;
215    }
216    
217    /**
218     * Get possible incoming steps for action
219     * @param actionId the current action's id
220     * @param workflowDescriptor the current  workflow
221     * @return a set of the action's incoming steps
222     */
223    public Set<StepDescriptor> getIncomingSteps(int actionId, WorkflowDescriptor workflowDescriptor)
224    {
225        Set<StepDescriptor> incomingSteps = new HashSet<>();
226        List<StepDescriptor> steps = workflowDescriptor.getSteps();
227        for (StepDescriptor step : steps)
228        {
229            if (step.getAction(actionId) != null)
230            {
231                incomingSteps.add(step);
232            }
233        }
234        return incomingSteps;
235    }
236    
237    /**
238     * Get current action's final steps and associated conditions
239     * @param currentNode id of current node
240     * @param workflowName unique name of current workflow
241     * @param actionId id of current action
242     * @return a map of current node's children
243     */
244    @Callable(right = "Workflow_Right_Read")
245    public Map<String, Object> getFinalSteps(String currentNode, String workflowName, String actionId) 
246    {
247        WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName);
248        ActionDescriptor action = workflowDescriptor.getAction(Integer.valueOf(actionId));
249        
250        List<Map<String, Object>> nodes = new ArrayList<>();
251        
252        List<ConditionalResultDescriptor> conditionalResults = action.getConditionalResults();
253        
254        if (currentNode.equals("root")) //get results(steps) nodes
255        {
256            ResultDescriptor unconditionalResult = action.getUnconditionalResult();
257            nodes.add(_step2JSON(workflowDescriptor, unconditionalResult.getStep(), false));
258            
259            for (ConditionalResultDescriptor result : conditionalResults)
260            {
261                StepDescriptor conditionalFinalStep = workflowDescriptor.getStep(result.getStep());
262                nodes.add(_step2JSON(workflowDescriptor, conditionalFinalStep.getId(), true));
263            }
264        }
265        else //get conditions nodes,
266        {
267            //FIXME this doesn't manage case where there are nested conditions because they don't exist yet
268            int stepId = _convertStepIdAsInteger(currentNode);
269            for (ConditionsDescriptor conditionWrapper: _getResultConditions(conditionalResults, stepId))
270            {
271                List<AbstractDescriptor> conditions = conditionWrapper.getConditions();
272                for (int i = 0; i < conditions.size(); i++)
273                {
274                    nodes.add(_workflowConditionDAO.conditionToJSON(conditions.get(i), currentNode, i));
275                }
276            }
277        }
278        
279        return Map.of("results", nodes);
280    }
281
282    private Integer _convertStepIdAsInteger(String stepId)
283    {
284        return Integer.valueOf(stepId.substring(4));
285    }
286    
287    private List<ConditionsDescriptor> _getResultConditions(List<ConditionalResultDescriptor> conditionalResults, int stepId)
288    {
289        return conditionalResults.stream()
290                    .filter(r -> r.getStep() == stepId)
291                    .map(r -> r.getConditions())
292                    .findFirst()
293                    .orElse(List.of());
294    }
295    
296    /**
297     * Get step infos
298     * @param workflowDescriptor current workflow
299     * @param stepId id of current step
300     * @param hasChildren true if step has actions 
301     * @return a map of the step infos
302     */
303    protected Map<String, Object> _step2JSON(WorkflowDescriptor workflowDescriptor, int stepId, boolean hasChildren)
304    {
305        Map<String, Object> infos = new HashMap<>();
306        
307        infos.put("id", "step" + stepId);
308        infos.put("elementId", stepId);
309        infos.put("label", getStepLabel(workflowDescriptor, stepId));
310        infos.put("elementType", "step");
311        infos.put("hasChildren", hasChildren);
312        try
313        {
314            infos.put("smallIcon", getStepIconPath(workflowDescriptor, stepId));
315        }
316        catch (Exception e)
317        {
318            getLogger().error("An error occurred while getting icon path for step id {}", stepId, e);
319        }
320       
321        return infos;
322    }
323    
324    /**
325     * Get the translated step label
326     * @param workflowDescriptor current workflow
327     * @param stepId id of current step
328     * @return the step label as string
329     */
330    public String getStepLabel(WorkflowDescriptor workflowDescriptor, int stepId)
331    {
332        I18nizableText label = _getStepLabel(workflowDescriptor, stepId);
333        return _translateKey(label, DEFAULT_STEP_NAME);
334    }
335    
336    /**
337     * Get the step's icon path 
338     * @param workflowDescriptor current worklfow
339     * @param stepId id of current step
340     * @return the icon path
341     */
342    public String getStepIconPath(WorkflowDescriptor workflowDescriptor, int stepId) 
343    {
344        I18nizableText label = _getStepLabel(workflowDescriptor, stepId);
345        return _workflowHelper.getElementIconPath(label, __DEFAULT_STEP_ICON_PATH);
346    }
347    
348    /**
349     * Get the step's icon path as base 64 for svg links
350     * @param workflowDescriptor current worklfow
351     * @param stepId id of current step
352     * @return the icon path as base 64
353     */
354    public String getStepIconPathAsBase64(WorkflowDescriptor workflowDescriptor, int stepId) 
355    {
356        I18nizableText label = _getStepLabel(workflowDescriptor, stepId);
357        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_STEP_ICON_PATH);
358    }
359    
360    private I18nizableText _getStepLabel(WorkflowDescriptor workflowDescriptor, int stepId)
361    {
362        switch (stepId)
363        {
364            case -1:
365                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_RESULTS_SAME_STEP");
366            case 0:
367                return new I18nizableText("plugin.workflow", "PLUGINS_WORKFLOW_INITIAL_STEP_NAME");
368            default:
369                StepDescriptor step = workflowDescriptor.getStep(stepId);
370                return new I18nizableText("application", step.getName());
371        }
372    }
373    
374    /**
375     * Get the translated action label
376     * @param action current action
377     * @return the action label 
378     */
379    public String getActionLabel(ActionDescriptor action)
380    {
381        I18nizableText label = _getActionLabel(action);
382        return _translateKey(label, DEFAULT_ACTION_NAME);
383    }
384    
385    /**
386     * Get the action's icon path 
387     * @param action current action
388     * @return the icon's path
389     */
390    public String getActionIconPath(ActionDescriptor action)
391    {
392        I18nizableText label = _getActionLabel(action);
393        return _workflowHelper.getElementIconPath(label, __DEFAULT_ACTION_ICON_PATH);
394    }
395    
396    /**
397     * Get the action's icon path as base 64 for svg's links
398     * @param action current action
399     * @return the icon's path as base 64
400     */
401    public String getActionIconPathAsBase64(ActionDescriptor action) 
402    {
403        I18nizableText label = _getActionLabel(action);
404        return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_ACTION_ICON_PATH);
405    }
406    
407    private I18nizableText _getActionLabel(ActionDescriptor action)
408    {
409        return new I18nizableText("application", action.getName());
410    }
411    
412    /**
413     * Translate i18n label for workflow element, return a default name if translation is not found
414     * @param key an i18n key pointing to workflow element's label 
415     * @return a translated label 
416     */
417    private String _translateKey(I18nizableText key, I18nizableText defaultKey)
418    {
419        String translate = _i18nUtils.translate(key);
420        return StringUtil.isBlank(translate)
421                ? defaultKey != null 
422                    ? _i18nUtils.translate(defaultKey)
423                    : null
424                : translate;
425    }
426    
427    private record StepWithIcon(Integer id, String label, String iconPath) { /* empty */ }
428
429}