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.Optional; 024import java.util.Set; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031 032import org.ametys.core.ui.Callable; 033import org.ametys.plugins.workflow.component.WorkflowLanguageManager; 034import org.ametys.plugins.workflow.support.I18nHelper; 035import org.ametys.plugins.workflow.support.WorflowRightHelper; 036import org.ametys.plugins.workflow.support.WorkflowHelper; 037import org.ametys.plugins.workflow.support.WorkflowSessionHelper; 038import org.ametys.runtime.i18n.I18nizableText; 039 040import com.opensymphony.workflow.loader.ActionDescriptor; 041import com.opensymphony.workflow.loader.DescriptorFactory; 042import com.opensymphony.workflow.loader.ResultDescriptor; 043import com.opensymphony.workflow.loader.StepDescriptor; 044import com.opensymphony.workflow.loader.WorkflowDescriptor; 045 046/** 047 * The workflow action DAO 048 */ 049public class WorkflowTransitionDAO implements Component, Serviceable 050{ 051 /** The component's role */ 052 public static final String ROLE = WorkflowTransitionDAO.class.getName(); 053 054 /** The default label for actions */ 055 public static final I18nizableText DEFAULT_ACTION_NAME = new I18nizableText("plugin.workflow", "PLUGIN_WORKFLOW_DEFAULT_ACTION_LABEL"); 056 057 /** Default path for svg action icons */ 058 private static final String __DEFAULT_SVG_ACTION_ICON_PATH = "plugin:cms://resources/img/history/workflow/action_0_16.png"; 059 060 /** Default path for node action icons */ 061 private static final String __DEFAULT_ACTION_ICON_PATH = "/plugins/cms/resources/img/history/workflow/action_0_16.png"; 062 063 /** The workflow session helper */ 064 protected WorkflowSessionHelper _workflowSessionHelper; 065 066 /** The workflow right helper */ 067 protected WorflowRightHelper _workflowRightHelper; 068 069 /** The Workflow Language Manager */ 070 protected WorkflowLanguageManager _workflowLanguageManager; 071 072 /** The workflow helper */ 073 protected WorkflowHelper _workflowHelper; 074 075 /** The workflow step DAO */ 076 protected WorkflowStepDAO _workflowStepDAO; 077 078 /** The helper for i18n translations and catalogs */ 079 protected I18nHelper _i18nHelper; 080 081 /** The context */ 082 protected Context _context; 083 084 public void service(ServiceManager smanager) throws ServiceException 085 { 086 _workflowSessionHelper = (WorkflowSessionHelper) smanager.lookup(WorkflowSessionHelper.ROLE); 087 _workflowHelper = (WorkflowHelper) smanager.lookup(WorkflowHelper.ROLE); 088 _workflowRightHelper = (WorflowRightHelper) smanager.lookup(WorflowRightHelper.ROLE); 089 _i18nHelper = (I18nHelper) smanager.lookup(I18nHelper.ROLE); 090 _workflowLanguageManager = (WorkflowLanguageManager) smanager.lookup(WorkflowLanguageManager.ROLE); 091 _workflowStepDAO = (WorkflowStepDAO) smanager.lookup(WorkflowStepDAO.ROLE); 092 } 093 094 /** 095 * Get the transition infos to initialize creation/edition form fields 096 * @param workflowName the current workflow name 097 * @param transitionId the current transition's id, can be null 098 * @return the transition values and a list of taken ids 099 */ 100 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 101 public Map<String, Object> getTransitionInfos(String workflowName, Integer transitionId) 102 { 103 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false); 104 _workflowRightHelper.checkEditRight(workflowDescriptor); 105 Map<String, Object> transitionInfos = new HashMap<>(); 106 Set<Integer> transitionIds = _workflowHelper.getAllActions(workflowDescriptor); 107 I18nizableText labelKey = DEFAULT_ACTION_NAME; 108 if (transitionId != null) 109 { 110 transitionIds.remove(transitionId); 111 transitionInfos.put("id", transitionId); 112 ActionDescriptor action = workflowDescriptor.getAction(transitionId); 113 labelKey = getActionLabel(action); 114 115 transitionInfos.put("finalStep", action.getUnconditionalResult().getStep()); 116 } 117 transitionInfos.put("ids", transitionIds); 118 119 Map<String, String> translations = _workflowSessionHelper.getTranslation(workflowName, labelKey); 120 if (translations == null) 121 { 122 translations = new HashMap<>(); 123 translations.put(_workflowLanguageManager.getCurrentLanguage(), _i18nHelper.translateKey(workflowDescriptor.getName(), labelKey, DEFAULT_ACTION_NAME)); 124 } 125 transitionInfos.put("labels", translations); 126 127 return transitionInfos; 128 } 129 130 /** 131 * Get a set of transitions already defined in current workflow beside initialActions 132 * @param workflowName the current workflow name 133 * @param parentStepId the current selected step 134 * @return a set containing maps of actions properties 135 */ 136 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 137 public Set<Map<String, Object>> getAvailableActions(String workflowName, Integer parentStepId) 138 { 139 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false); 140 _workflowRightHelper.checkReadRight(workflowDescriptor); 141 Set<Map<String, Object>> availableActions = new HashSet<>(); 142 if (0 != parentStepId) // generic actions can't be used as initial actions 143 { 144 StepDescriptor parentStep = workflowDescriptor.getStep(parentStepId); 145 List<ActionDescriptor> actions = parentStep.getActions(); 146 147 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 148 for (StepDescriptor step: steps) 149 { 150 List<ActionDescriptor> outgoingActions = step.getActions(); 151 for (ActionDescriptor outgoingAction: outgoingActions) 152 { 153 if (!actions.contains(outgoingAction)) 154 { 155 availableActions.add(_getActionInfos(workflowName, outgoingAction)); 156 } 157 } 158 } 159 } 160 return availableActions; 161 } 162 163 private Map<String, Object> _getActionInfos(String workflowName, ActionDescriptor action) 164 { 165 Map<String, Object> actionInfos = new HashMap<>(); 166 int actionId = action.getId(); 167 actionInfos.put("id", actionId); 168 actionInfos.put("label", getActionLabel(workflowName, action) + " (" + actionId + ")"); 169 return actionInfos; 170 } 171 172 /** 173 * Create a new transition 174 * @param workflowName the current workflow name 175 * @param parentStepId the parent step id 176 * @param transitionId the id for the transition to create 177 * @param labels the multilinguals labels for the transition 178 * @param finalStepId the id for the transition's unconditional result 179 * @return a map with error message or with transition's id if succesfull 180 */ 181 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 182 public Map<String, Object> createTransition(String workflowName, Integer parentStepId, int transitionId, Map<String, String> labels, int finalStepId) 183 { 184 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 185 _workflowRightHelper.checkEditRight(workflowDescriptor); 186 187 if (_workflowHelper.getAllActions(workflowDescriptor).contains(transitionId)) 188 { 189 return Map.of("message", "duplicate-id"); 190 } 191 192 DescriptorFactory factory = new DescriptorFactory(); 193 ActionDescriptor action = factory.createActionDescriptor(); 194 action.setId(transitionId); 195 I18nizableText actionNameI18nKey = _i18nHelper.generateI18nKey(workflowName, "ACTION", transitionId); 196 action.setName(actionNameI18nKey.toString()); 197 ResultDescriptor finalStep = factory.createResultDescriptor(); 198 finalStep.setStep(finalStepId); 199 action.setUnconditionalResult(finalStep); 200 201 if (isInitialStep(parentStepId)) 202 { 203 workflowDescriptor.addInitialAction(action); 204 } 205 else 206 { 207 StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId); 208 stepDescriptor.getActions().add(action); 209 } 210 211 _workflowSessionHelper.updateTranslations(workflowName, actionNameI18nKey, labels); 212 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 213 214 return _getActionProperties(workflowDescriptor, action, parentStepId); 215 } 216 217 private Map<String, Object> _getActionProperties(WorkflowDescriptor workflowDescriptor, ActionDescriptor action, Integer stepId) 218 { 219 Map<String, Object> results = new HashMap<>(); 220 results.put("actionId", action.getId()); 221 results.put("actionLabels", getActionLabel(workflowDescriptor.getName(), action)); 222 results.put("stepId", stepId); 223 results.put("stepLabel", _workflowStepDAO.getStepLabel(workflowDescriptor, stepId)); 224 results.put("workflowId", workflowDescriptor.getName()); 225 226 return results; 227 } 228 229 private boolean isInitialStep(Integer stepIdToInt) 230 { 231 return stepIdToInt == 0; 232 } 233 234 /** 235 * Add an existing transition to current step 236 * @param workflowName the current workflow name 237 * @param parentStepId the current selected step id 238 * @param transitionIds a list of ids corresponding to the transitions to add to current step 239 * @return the first transition id and its parent sted id 240 */ 241 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 242 public Map<String, Object> addTransitions(String workflowName, Integer parentStepId, List<Integer> transitionIds) 243 { 244 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 245 _workflowRightHelper.checkEditRight(workflowDescriptor); 246 Map<String, Object> results = new HashMap<>(); 247 248 if (parentStepId != 0) 249 { 250 StepDescriptor step = workflowDescriptor.getStep(parentStepId); 251 for (Integer id : transitionIds) 252 { 253 ActionDescriptor action = _getAction(workflowDescriptor, id); 254 step.getActions().add(action); 255 if (!action.isCommon()) 256 { 257 _updateWorkflowCommonAction(workflowDescriptor, id, action); 258 } 259 else 260 { 261 step.getCommonActions().add(id); 262 } 263 } 264 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 265 results = _getActionProperties(workflowDescriptor, workflowDescriptor.getAction(transitionIds.get(0)), parentStepId); 266 } 267 else 268 { 269 results.put("message", "initial-step"); 270 } 271 272 return results; 273 } 274 275 private ActionDescriptor _getAction(WorkflowDescriptor workflowDescriptor, Integer actionId) 276 { 277 ActionDescriptor action = workflowDescriptor.getAction(actionId); 278 if (action == null) 279 { 280 Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions(); 281 action = commonActions.get(actionId); 282 } 283 return action; 284 } 285 286 /** 287 * Set action as 'common' in steps using it 288 * @param workflowDescriptor the current workflow 289 * @param actionId id of current action 290 * @param action the action 291 */ 292 protected void _updateWorkflowCommonAction(WorkflowDescriptor workflowDescriptor, int actionId, ActionDescriptor action) 293 { 294 List<StepDescriptor> stepsToUpdate = new ArrayList<>(); 295 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 296 297 //remove individual action from steps 298 for (StepDescriptor step : steps) 299 { 300 if (step.getAction(actionId) != null) 301 { 302 step.getActions().remove(action); 303 stepsToUpdate.add(step); 304 } 305 } 306 //set action as common in workflow 307 workflowDescriptor.addCommonAction(action); 308 309 //put back action in steps as common action 310 for (StepDescriptor step : stepsToUpdate) 311 { 312 step.getCommonActions().add(actionId); 313 step.getActions().add(action); 314 } 315 } 316 317 /** 318 * Rename the transition 319 * @param workflowName the workflow name 320 * @param stepId id of selected step 321 * @param actionId the transition's id 322 * @param newMainLabel the new label in current language 323 * @return a map with error message or with transition's infos if succesfull 324 */ 325 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 326 public Map<String, Object> editTransitionLabel(String workflowName, Integer stepId, Integer actionId, String newMainLabel) 327 { 328 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 329 _workflowRightHelper.checkEditRight(workflowDescriptor); 330 331 ActionDescriptor action = workflowDescriptor.getAction(actionId); 332 _updateActionName(workflowName, action); 333 I18nizableText actionKey = getActionLabel(action); 334 _workflowSessionHelper.updateTranslations(workflowName, actionKey, Map.of(_workflowLanguageManager.getCurrentLanguage(), newMainLabel)); 335 336 return _getActionProperties(workflowDescriptor, action, stepId); 337 } 338 339 private void _updateActionName(String workflowName, ActionDescriptor action) 340 { 341 String defaultCatalog = _workflowHelper.getWorkflowCatalog(workflowName); 342 I18nizableText actionKey = getActionLabel(action); 343 344 if (!defaultCatalog.equals(actionKey.getCatalogue())) 345 { 346 String newName = new I18nizableText(defaultCatalog, actionKey.getKey()).toString(); 347 action.setName(newName); 348 } 349 } 350 351 /** 352 * Edit the transition 353 * @param workflowName the workflow name 354 * @param stepId id of selected step 355 * @param oldId the transition's former id 356 * @param newId the transition's new id 357 * @param labels the transition's multilingual labels 358 * @param finalStepId the transition's unconditional result id 359 * @return a map with error message or with transition's infos if succesfull 360 */ 361 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 362 public Map<String, Object> editTransition(String workflowName, Integer stepId, Integer oldId, Integer newId, Map<String, String> labels, Integer finalStepId) 363 { 364 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 365 _workflowRightHelper.checkEditRight(workflowDescriptor); 366 367 ActionDescriptor action = workflowDescriptor.getAction(oldId); 368 369 if (!newId.equals(oldId)) 370 { 371 if (_workflowHelper.getAllActions(workflowDescriptor).contains(newId)) 372 { 373 return Map.of("message", "duplicate-id"); 374 } 375 if (action.isCommon()) 376 { 377 //Don't change edit order below: first update steps then update action 378 workflowDescriptor.getCommonActions().remove(oldId, action); 379 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 380 for (StepDescriptor step: steps) 381 { 382 if (step.getAction(oldId) != null) 383 { 384 List<Integer> commonActions = step.getCommonActions(); 385 commonActions.remove(commonActions.indexOf(oldId)); 386 commonActions.add(newId); 387 } 388 } 389 action.setId(newId); 390 workflowDescriptor.getCommonActions().put(newId, action); 391 } 392 else 393 { 394 action.setId(newId); 395 } 396 } 397 398 DescriptorFactory factory = new DescriptorFactory(); 399 ResultDescriptor finalStep = factory.createResultDescriptor(); 400 finalStep.setStep(finalStepId); 401 action.setUnconditionalResult(finalStep); 402 403 _updateActionName(workflowName, action); 404 I18nizableText actionKey = getActionLabel(action); 405 _workflowSessionHelper.updateTranslations(workflowName, actionKey, labels); 406 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 407 408 return _getActionProperties(workflowDescriptor, action, stepId); 409 } 410 411 /** 412 * Remove transition from step 413 * @param workflowName the current workflow name 414 * @param parentStepId the parent step id to remove the transition from 415 * @param transitionId the id for the transition to remove 416 * @return empty map if successfull 417 */ 418 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 419 public Map<String, Object> removeTransition(String workflowName, Integer parentStepId, Integer transitionId) 420 { 421 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 422 _workflowRightHelper.checkEditRight(workflowDescriptor); 423 ActionDescriptor actionToRemove = null; 424 if (isInitialStep(parentStepId)) 425 { 426 actionToRemove = workflowDescriptor.getInitialAction(transitionId); 427 _workflowSessionHelper.removeTranslation(workflowName, new I18nizableText("application", actionToRemove.getName())); 428 workflowDescriptor.getInitialActions().remove(actionToRemove); 429 } 430 else 431 { 432 StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId); 433 actionToRemove = workflowDescriptor.getAction(transitionId); 434 stepDescriptor.getActions().remove(actionToRemove); 435 if (actionToRemove.isCommon()) 436 { 437 List<Integer> commonActions = stepDescriptor.getCommonActions(); 438 commonActions.remove(commonActions.indexOf(transitionId)); 439 _manageCommonAction(workflowDescriptor, actionToRemove); 440 } 441 else 442 { 443 I18nizableText actionKey = getActionLabel(actionToRemove); 444 _workflowSessionHelper.removeTranslation(workflowName, actionKey); //use actionKey instead of action.getName() in case plugin's name is in the name 445 } 446 } 447 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 448 449 return _getActionProperties(workflowDescriptor, actionToRemove, parentStepId); 450 } 451 452 /** 453 * Verify that the action is still used by multiple steps. If not anymore, replace the action by a non common copy 454 * @param workflowDescriptor the current workflow 455 * @param action the action removed 456 */ 457 protected void _manageCommonAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor action) 458 { 459 Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions(); 460 Integer id = action.getId(); 461 if (commonActions.containsKey(id)) 462 { 463 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 464 int numberOfUse = _getNumberOfUse(id, steps); 465 if (numberOfUse == 1) 466 { 467 //only way to unset common in action is to replace it with a new copy 468 StepDescriptor parentStep = _getParentStep(steps, id).get(); 469 List<Integer> stepCommonActions = parentStep.getCommonActions(); 470 stepCommonActions.remove(stepCommonActions.indexOf(id)); 471 parentStep.getActions().remove(action); 472 workflowDescriptor.getCommonActions().remove(id, action); 473 ActionDescriptor actionCopy = _copyAction(action); 474 parentStep.getActions().add(actionCopy); 475 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 476 } 477 } 478 } 479 480 private Optional<StepDescriptor> _getParentStep(List<StepDescriptor> steps, int actionId) 481 { 482 return steps.stream().filter(s -> s.getAction(actionId) != null).findFirst(); 483 } 484 485 private ActionDescriptor _copyAction(ActionDescriptor actionToCopy) 486 { 487 DescriptorFactory factory = new DescriptorFactory(); 488 ActionDescriptor action = factory.createActionDescriptor(); 489 action.setId(actionToCopy.getId()); 490 action.setName(actionToCopy.getName()); 491 action.setUnconditionalResult(actionToCopy.getUnconditionalResult()); 492 action.getConditionalResults().addAll(actionToCopy.getConditionalResults()); 493 action.setMetaAttributes(actionToCopy.getMetaAttributes()); 494 action.setRestriction(actionToCopy.getRestriction()); 495 action.getPreFunctions().addAll(actionToCopy.getPreFunctions()); 496 action.getPostFunctions().addAll(actionToCopy.getPostFunctions()); 497 498 return action; 499 } 500 501 /** 502 * Get the number of steps using the action 503 * @param workflowName the current workflow's name 504 * @param actionId the current action's id 505 * @return the number of steps using the action 506 */ 507 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 508 public int getNumberOfUse(String workflowName, Integer actionId) 509 { 510 WorkflowDescriptor workflow = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false); 511 return _getNumberOfUse(actionId, workflow.getSteps()); 512 } 513 514 private int _getNumberOfUse(Integer actionId, List<StepDescriptor> steps) 515 { 516 int stepSize = steps.size(); 517 int numberOfUse = 0; 518 int index = 0; 519 while (index < stepSize) 520 { 521 if (steps.get(index).getAction(actionId) != null) 522 { 523 numberOfUse++; 524 } 525 index++; 526 } 527 return numberOfUse; 528 } 529 530 /** 531 * Get the translated action label 532 * @param workflowName the workflow's unique name 533 * @param action current action 534 * @return the action label 535 */ 536 public String getActionLabel(String workflowName, ActionDescriptor action) 537 { 538 I18nizableText label = getActionLabel(action); 539 return _i18nHelper.translateKey(workflowName, label, DEFAULT_ACTION_NAME); 540 } 541 542 /** 543 * Get the action's icon path 544 * @param workflowName name of current workflow 545 * @param action current action 546 * @return the icon's path 547 */ 548 public String getActionIconPath(String workflowName, ActionDescriptor action) 549 { 550 I18nizableText label = getActionLabel(action); 551 label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 552 return _workflowHelper.getElementIconPath(label, __DEFAULT_ACTION_ICON_PATH); 553 } 554 555 /** 556 * Get the action's icon path as base 64 for svg's links 557 * @param workflowName name of current workflow 558 * @param action current action 559 * @return the icon's path as base 64 560 */ 561 public String getActionIconPathAsBase64(String workflowName, ActionDescriptor action) 562 { 563 I18nizableText label = getActionLabel(action); 564 label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 565 return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_ACTION_ICON_PATH); 566 } 567 568 /** 569 * Get the action label as i18n 570 * @param action the current action 571 * @return the label as i18n 572 */ 573 public I18nizableText getActionLabel(ActionDescriptor action) 574 { 575 return new I18nizableText("application", action.getName()); 576 } 577}