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