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()), "\"", """); 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}