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