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