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(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 != oldId) 370 { 371 if (_workflowHelper.getAllActions(workflowDescriptor).contains(newId)) 372 { 373 374 return Map.of("message", "duplicate-id"); 375 } 376 if (action.isCommon()) 377 { 378 //Don't change edit order below: first update steps then update action 379 workflowDescriptor.getCommonActions().remove(oldId, action); 380 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 381 for (StepDescriptor step: steps) 382 { 383 if (step.getAction(oldId) != null) 384 { 385 List<Integer> commonActions = step.getCommonActions(); 386 commonActions.remove(commonActions.indexOf(oldId)); 387 commonActions.add(newId); 388 } 389 } 390 action.setId(newId); 391 workflowDescriptor.getCommonActions().put(newId, action); 392 } 393 else 394 { 395 action.setId(newId); 396 } 397 } 398 399 DescriptorFactory factory = new DescriptorFactory(); 400 ResultDescriptor finalStep = factory.createResultDescriptor(); 401 finalStep.setStep(finalStepId); 402 action.setUnconditionalResult(finalStep); 403 404 _updateActionName(workflowName, action); 405 I18nizableText actionKey = getActionLabel(action); 406 _workflowSessionHelper.updateTranslations(workflowName, actionKey, labels); 407 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 408 409 return _getActionProperties(workflowDescriptor, action, stepId); 410 } 411 412 /** 413 * Remove transition from step 414 * @param workflowName the current workflow name 415 * @param parentStepId the parent step id to remove the transition from 416 * @param transitionId the id for the transition to remove 417 * @return empty map if successfull 418 */ 419 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 420 public Map<String, Object> removeTransition(String workflowName, Integer parentStepId, Integer transitionId) 421 { 422 WorkflowDescriptor workflowDescriptor = _workflowSessionHelper.getWorkflowDescriptor(workflowName, true); 423 _workflowRightHelper.checkEditRight(workflowDescriptor); 424 ActionDescriptor actionToRemove = null; 425 if (isInitialStep(parentStepId)) 426 { 427 actionToRemove = workflowDescriptor.getInitialAction(transitionId); 428 _workflowSessionHelper.removeTranslation(workflowName, new I18nizableText("application", actionToRemove.getName())); 429 workflowDescriptor.getInitialActions().remove(actionToRemove); 430 } 431 else 432 { 433 StepDescriptor stepDescriptor = workflowDescriptor.getStep(parentStepId); 434 actionToRemove = workflowDescriptor.getAction(transitionId); 435 stepDescriptor.getActions().remove(actionToRemove); 436 if (actionToRemove.isCommon()) 437 { 438 List<Integer> commonActions = stepDescriptor.getCommonActions(); 439 commonActions.remove(commonActions.indexOf(transitionId)); 440 _manageCommonAction(workflowDescriptor, actionToRemove); 441 } 442 else 443 { 444 I18nizableText actionKey = getActionLabel(actionToRemove); 445 _workflowSessionHelper.removeTranslation(workflowName, actionKey); //use actionKey instead of action.getName() in case plugin's name is in the name 446 } 447 } 448 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 449 450 return _getActionProperties(workflowDescriptor, actionToRemove, parentStepId); 451 } 452 453 /** 454 * Verify that the action is still used by multiple steps. If not anymore, replace the action by a non common copy 455 * @param workflowDescriptor the current workflow 456 * @param action the action removed 457 */ 458 protected void _manageCommonAction(WorkflowDescriptor workflowDescriptor, ActionDescriptor action) 459 { 460 Map<Integer, ActionDescriptor> commonActions = workflowDescriptor.getCommonActions(); 461 Integer id = action.getId(); 462 if (commonActions.containsKey(id)) 463 { 464 List<StepDescriptor> steps = workflowDescriptor.getSteps(); 465 int numberOfUse = _getNumberOfUse(id, steps); 466 if (numberOfUse == 1) 467 { 468 //only way to unset common in action is to replace it with a new copy 469 StepDescriptor parentStep = _getParentStep(steps, id).get(); 470 List<Integer> stepCommonActions = parentStep.getCommonActions(); 471 stepCommonActions.remove(stepCommonActions.indexOf(id)); 472 parentStep.getActions().remove(action); 473 workflowDescriptor.getCommonActions().remove(id, action); 474 ActionDescriptor actionCopy = _copyAction(action); 475 parentStep.getActions().add(actionCopy); 476 _workflowSessionHelper.updateWorkflowDescriptor(workflowDescriptor); 477 } 478 } 479 } 480 481 private Optional<StepDescriptor> _getParentStep(List<StepDescriptor> steps, int actionId) 482 { 483 return steps.stream().filter(s -> s.getAction(actionId) != null).findFirst(); 484 } 485 486 private ActionDescriptor _copyAction(ActionDescriptor actionToCopy) 487 { 488 DescriptorFactory factory = new DescriptorFactory(); 489 ActionDescriptor action = factory.createActionDescriptor(); 490 action.setId(actionToCopy.getId()); 491 action.setName(actionToCopy.getName()); 492 action.setUnconditionalResult(actionToCopy.getUnconditionalResult()); 493 action.getConditionalResults().addAll(actionToCopy.getConditionalResults()); 494 action.setMetaAttributes(actionToCopy.getMetaAttributes()); 495 action.setRestriction(actionToCopy.getRestriction()); 496 action.getPreFunctions().addAll(actionToCopy.getPreFunctions()); 497 action.getPostFunctions().addAll(actionToCopy.getPostFunctions()); 498 499 return action; 500 } 501 502 /** 503 * Get the number of steps using the action 504 * @param workflowName the current workflow's name 505 * @param actionId the current action's id 506 * @return the number of steps using the action 507 */ 508 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 509 public int getNumberOfUse(String workflowName, Integer actionId) 510 { 511 WorkflowDescriptor workflow = _workflowSessionHelper.getWorkflowDescriptor(workflowName, false); 512 return _getNumberOfUse(actionId, workflow.getSteps()); 513 } 514 515 private int _getNumberOfUse(Integer actionId, List<StepDescriptor> steps) 516 { 517 int stepSize = steps.size(); 518 int numberOfUse = 0; 519 int index = 0; 520 while (index < stepSize) 521 { 522 if (steps.get(index).getAction(actionId) != null) 523 { 524 numberOfUse++; 525 } 526 index++; 527 } 528 return numberOfUse; 529 } 530 531 /** 532 * Get the translated action label 533 * @param workflowName the workflow's unique name 534 * @param action current action 535 * @return the action label 536 */ 537 public String getActionLabel(String workflowName, ActionDescriptor action) 538 { 539 I18nizableText label = getActionLabel(action); 540 return _i18nHelper.translateKey(workflowName, label, DEFAULT_ACTION_NAME); 541 } 542 543 /** 544 * Get the action's icon path 545 * @param workflowName name of current workflow 546 * @param action current action 547 * @return the icon's path 548 */ 549 public String getActionIconPath(String workflowName, ActionDescriptor action) 550 { 551 I18nizableText label = getActionLabel(action); 552 label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 553 return _workflowHelper.getElementIconPath(label, __DEFAULT_ACTION_ICON_PATH); 554 } 555 556 /** 557 * Get the action's icon path as base 64 for svg's links 558 * @param workflowName name of current workflow 559 * @param action current action 560 * @return the icon's path as base 64 561 */ 562 public String getActionIconPathAsBase64(String workflowName, ActionDescriptor action) 563 { 564 I18nizableText label = getActionLabel(action); 565 label = _workflowSessionHelper.getOldLabelKeyIfCloned(workflowName, label); 566 return _workflowHelper.getElementIconAsBase64(label, __DEFAULT_SVG_ACTION_ICON_PATH); 567 } 568 569 /** 570 * Get the action label as i18n 571 * @param action the current action 572 * @return the label as i18n 573 */ 574 public I18nizableText getActionLabel(ActionDescriptor action) 575 { 576 return new I18nizableText("application", action.getName()); 577 } 578}