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.readers;
017
018import java.util.List;
019
020import org.apache.cocoon.environment.Request;
021import org.apache.commons.lang3.StringUtils;
022
023import org.ametys.plugins.workflow.dao.WorkflowStepDAO;
024
025import com.opensymphony.workflow.loader.ActionDescriptor;
026import com.opensymphony.workflow.loader.ConditionalResultDescriptor;
027import com.opensymphony.workflow.loader.ResultDescriptor;
028import com.opensymphony.workflow.loader.StepDescriptor;
029import com.opensymphony.workflow.loader.WorkflowDescriptor;
030
031/**
032 * Serialize PlantUML state diagram
033 */
034public class PlantUMLSVGReader extends AbstractPlantUMLSVGReader 
035{
036    @Override
037    protected String _getPlantUMLType()
038    {
039        return "uml";
040    }
041
042    @Override
043    protected String _getPlantUMLStyle()
044    {
045        StringBuilder style = new StringBuilder();
046        
047        style.append("skinparam HyperLinkUnderline false \n");
048        style.append("skinparam HyperLinkColor #0a7fb2 \n");
049        style.append("skinparam State { \n");
050        style.append("BorderThickness 1.5 \n");
051        style.append("} \n");
052        style.append("hide empty description\n");
053        
054        return style.toString();
055    }
056
057    @Override
058    protected String _getPlantUMLGraphContent(Request request, WorkflowDescriptor workflowDescriptor)
059    {
060        List<StepDescriptor> steps = workflowDescriptor.getSteps();
061        StringBuilder content = new StringBuilder();
062        
063        for (StepDescriptor step : steps)
064        {
065            //create state node
066            content.append(_createState(workflowDescriptor, step));
067            
068            List<ActionDescriptor> actions = step.getActions();
069            for (ActionDescriptor action : actions)
070            {
071                //create state's outgoing transitions
072                content.append(_createTransition(workflowDescriptor, step, action));
073            }
074        }
075        
076        //create workflow's initials actions
077        List<ActionDescriptor> initialActions = workflowDescriptor.getInitialActions();
078        for (ActionDescriptor initialAction: initialActions)
079        {
080            ResultDescriptor unconditionalResult = initialAction.getUnconditionalResult();
081            String actionContent = _getTransitionArrow(
082                    "[*]",
083                    String.valueOf(unconditionalResult.getStep()),
084                    _getActionLabel(initialAction),
085                    _workflowStepDAO.getActionIconPathAsBase64(initialAction),
086                    _getJsFunction(workflowDescriptor.getName(), WorkflowStepDAO.INITIAL_STEP_ID, String.valueOf(initialAction.getId())),
087                    _getActionTooltip(initialAction),
088                    false
089                );
090            content.append(actionContent);
091        }
092        return content.toString();
093    }
094    
095    /**
096     * Get the transition arrow between 2 states
097     * @param incomingState state's id before transition
098     * @param outgoingState state's id after transition
099     * @param label the transition's label
100     * @param iconPath the transition's icon path
101     * @param jsFunction the js function for sending current transition as selection
102     * @param linkTooltip the transition link's tooltip
103     * @param isDashed true if the arrow must be dashed
104     * @return the transition arrow content
105     */
106    protected String _getTransitionArrow(String incomingState, String outgoingState, String label, String iconPath, String jsFunction, String linkTooltip, boolean isDashed)
107    {
108        StringBuilder nodeContent = new StringBuilder(incomingState);
109        nodeContent.append(isDashed ? " -[dashed]-> " : " --> ");
110        nodeContent.append(outgoingState);
111        if (StringUtils.isNotBlank(label))
112        {
113            nodeContent.append(": ");
114            nodeContent.append(" [[javascript:parent." + jsFunction + " ");
115            nodeContent.append("{" + linkTooltip + "}");
116            
117            if (StringUtils.isNotBlank(iconPath))
118            {
119                nodeContent.append(" <img:" + iconPath + ">");
120            }
121            
122            nodeContent.append(" " + label);
123            nodeContent.append("]]");
124        }
125        nodeContent.append(" \n");
126        
127        return nodeContent.toString();
128    }
129    
130    /**
131     * Create new state in diagram
132     * @param workflow unique id of current workflow
133     * @param step a step of the workflow
134     * @return a string defining a plantUML state
135     */
136    protected String _createState(WorkflowDescriptor workflow, StepDescriptor step) 
137    {
138        String stepToUML = "";
139        
140        String steplabel = _getStepNodeLabel(workflow, step.getId());
141        String elementAbsoluteIconPath = _workflowStepDAO.getStepIconPathAsBase64(workflow, step.getId());
142        String stepId = String.valueOf(step.getId());
143        String toolip = " {" + _getStepTooltip(step)  + "}";
144        String icon = StringUtils.isBlank(elementAbsoluteIconPath)
145                ? StringUtils.EMPTY
146                : "<img:" + elementAbsoluteIconPath + "> ";
147        String jsFunction = " [[javascript:parent." + _getJsFunction(workflow.getName(), stepId, null)  + toolip + "]]";
148        stepToUML = "state \"" + icon + steplabel + "\" as " + stepId + jsFunction + __MAIN_STEP_NODE_COLOR + " \n";
149        
150        return stepToUML;
151    }
152    
153    /**
154     * Create a new transition between two states in diagram
155     * @param workflow unique id of current workflow
156     * @param step the initial step of this transition
157     * @param transition the current action
158     * @return a string defining a plantUML transition
159     */
160    protected String _createTransition(WorkflowDescriptor workflow, StepDescriptor step, ActionDescriptor transition)
161    {
162        StringBuilder transitionToUML = new StringBuilder();
163        
164        String transitionlabel = _getActionLabel(transition);
165        String tooltip = _getActionTooltip(transition);
166        String elementAbsoluteIconPath = _workflowStepDAO.getActionIconPathAsBase64(transition);
167        String jsFunction = _getJsFunction(workflow.getName(), String.valueOf(step.getId()), String.valueOf(transition.getId()));
168        String initialStep = String.valueOf(step.getId());
169        ResultDescriptor unconditionalResult = transition.getUnconditionalResult();
170        String finalStep = unconditionalResult.getStep() != -1 
171                ? String.valueOf(unconditionalResult.getStep())
172                : initialStep;
173        
174        List<ConditionalResultDescriptor> conditionalResults = transition.getConditionalResults();
175        if (!conditionalResults.isEmpty())
176        {
177            //create intermediary conditional state
178            String choiceStateName = "step" + initialStep + "action" + String.valueOf(transition.getId());
179            transitionToUML.append("state " + choiceStateName + " <<choice>> \n");
180            //create intermediary transition
181            String actionContent = _getTransitionArrow(initialStep, choiceStateName, transitionlabel, elementAbsoluteIconPath, jsFunction, tooltip, true);
182            transitionToUML.append(actionContent);
183            
184            //create unconditional transition
185            String unconditionalActionContent = _getTransitionArrow(choiceStateName, finalStep, null, null, null, null, false);
186            transitionToUML.append(unconditionalActionContent);
187            
188            //create conditionnal transitions
189            for (ConditionalResultDescriptor result : conditionalResults)
190            {
191                String conditionalStep = result.getStep() != -1 
192                        ? String.valueOf(result.getStep())
193                        : initialStep;
194                if (!conditionalStep.equals(finalStep)) //prevent duplicate transition arrow in graph
195                {
196                    String conditionalActionContent = _getTransitionArrow(choiceStateName, conditionalStep, null, null, null, null, false);
197                    transitionToUML.append(conditionalActionContent);
198                }
199            }
200        }
201        else
202        {
203            //create unconditional transition
204            String actionContent = _getTransitionArrow(initialStep, finalStep, transitionlabel, elementAbsoluteIconPath, jsFunction, tooltip, false);
205            transitionToUML.append(actionContent);
206        }
207        return transitionToUML.toString();
208    }
209}