001/* 002 * Copyright 2010 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.cms.clientsideelement; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.component.ComponentException; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.configuration.DefaultConfiguration; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.commons.collections.ListUtils; 032import org.slf4j.LoggerFactory; 033 034import org.ametys.cms.content.ContentHelper; 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.repository.WorkflowAwareContent; 037import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 038import org.ametys.core.ui.Callable; 039import org.ametys.core.ui.ClientSideElement; 040import org.ametys.core.ui.MenuClientSideElement; 041import org.ametys.core.ui.StaticClientSideElement; 042import org.ametys.core.ui.StaticFileImportsClientSideElement; 043import org.ametys.core.user.User; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.user.UserManager; 046import org.ametys.plugins.core.ui.util.ConfigurationHelper; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.lock.LockHelper; 049import org.ametys.plugins.repository.lock.LockableAmetysObject; 050import org.ametys.plugins.workflow.AbstractWorkflowComponent; 051import org.ametys.plugins.workflow.support.WorkflowHelper; 052import org.ametys.plugins.workflow.support.WorkflowProvider; 053import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 056 057import com.opensymphony.workflow.loader.ActionDescriptor; 058import com.opensymphony.workflow.loader.StepDescriptor; 059import com.opensymphony.workflow.loader.WorkflowDescriptor; 060import com.opensymphony.workflow.spi.Step; 061 062/** 063 * This element creates multiple toggle buttons representing a workflow. 064 * A menu represent workflow actions. 065 * 066 * The awaited configuration is: 067 * <workflow name="WORKFLOWNAME"> 068 * <action> 069 * <menu-1-label>I18N_KEY</menu-1-label> 070 * </action> 071 * 072 * <workflow-actions mode="exclude"> 073 * <action>22</action> 074 * <action>222</action> 075 * </workflow-actions> 076 * 077 * <workflow-steps mode="include"> 078 * <step>1</step> 079 * <step>2</step> 080 * </workflow-steps> 081 * 082 * <steps> 083 * <step id="1"> 084 * <workflow-actions mode="include"> 085 * <action>2</action> 086 * </workflow-actions> 087 * 088 * <comments mode="include"> 089 * <action>3</action> 090 * </comments> 091 * 092 * <action> 093 * <menu-1-label /> 094 * </action> 095 * </step> 096 * 097 * </steps> 098 * </workflow> 099 * 100 * Where WORKFLOWNAME is the name of the workflow such as 'content'. MANDATORY 101 * Where <action> is optional to override the default configuration of every step button and step menu 102 * The attribute name="..." is optional to specify the JS class used. 103 * Where <workflow-actions< is optional to restrict the available actions in the menu. Default value is <workflow-actions mode="exclude"/> 104 * The attribute mode="include" indicates that the list of actions ids will replace the one automatically determined (even if it only can by a sublist). Listing ids here ensure that no new actions will appear in this menu if the workflow is modified. 105 * The attribute mode="exclude" (default value) indicates that the list of actions ids will be removed from the one automatically determined (it only can by a sublist of it). Listing ids here ensure that when the workflow has a new action it will be added here. 106 * Where <steps> is optional and allows you to configure each step specifically. 107 * The <step> id attribute is MANDATORY 108 * The <workflow-actions> is identical to the one at the root of <workflow> but only for this specific step 109 * Where <comments< is identical to <workflow-actions< but to display to the user a dialog box to enter comments. The workflow has to support comments on that action. To avoid comments, set <comments mode="include"/> 110 * The <action> is identical to the one at the root of <workflow> but only for this specific step 111 * 112 * 113 * Here is a declaration sample in a plugin.xml file 114 * 115 * <extension id="org.ametys.web.workflow.WorkflowSteps" 116 * point="org.ametys.core.ui.RibbonControlsManager" 117 * class="org.ametys.cms.clientsideelement.WorkflowStepsClientSideElement"> 118 * <workflow name="content"> 119 * <steps> 120 * <step id="1"> <!-- Draft --> 121 * <workflow-actions mode="exclude"> 122 * <action>2</action><!-- Edit --> 123 * </workflow-actions> 124 * 125 * <comments mode="include"> 126 * <action>3</action><!-- propose --> 127 * </comments> 128 * </step> 129 * </steps> 130 * </workflow> 131 * </extension> 132 * 133 * Default configuration is proposed upon the StaticFileImportsClientSideElement. 134 * - default js class is "Ametys.plugins.cms.content.controller.WorkflowMenu" 135 * - default js files are loaded : /plugins/cms/resources/js/Ametys/plugins/cms/content/controller/WorkflowMenu.js and /plugins/cms/resources/js/Ametys/plugins/cms/content/actions/WorkflowAction.js 136 * - default js parameters are 137 * - label (to the i18nkey application:WORFKLOW_STEP_NAME) 138 * - description (to the i18nkey application:WORFKLOW_STEP_NAME_DESCRIPTION) 139 * - footerDescription (to the i18nkey application:WORFKLOW_STEP_NAME_FOOTER) 140 * - selection-target-id (to ^content$) 141 * - icon-small, icon-medium, icon-large (to /plugins/cms/resources_workflow/WORFKLOW_STEP_NAME-small.png for an application i18nkey, or /plugins/PLUGINNAME/resources/img/workflow/WORFKLOW_STEP_NAME-small.png for a plugin i18nkey) 142 * - workflow-step (tp the value configured avove) 143 * - workflow-name (to the value configured above) 144 * - selection-enable-multiselection (true) 145 * - selection-description-empty : i18nkey plugin.cms:CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION 146 * - selection-description-nomatch : i18nkey plugin.cms:CONTENT_WORKFLOW_NOMATCH_DESCRIPTION 147 * - selection-description-multiselectionforbidden : i18nkey plugin.cms:CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION 148 * - additionnal js parameters are 149 * - noaction-available-description : i18nkey for description when no actions are available, plugin.cms:CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION 150 * - refreshing-description : i18nkey for description when refreshing, plugin.cms:CONTENT_WORKFLOW_REFRESH_DESCRIPTION 151 * - contentselected-start-description : i18nkey for description of the current content selection (start of the description) : plugin.cms:CONTENT_WORKFLOW_DESCRIPTION_BEGIN 152 * - contentselected-end-description : i18nkey for description of the current content selection (end of the description) : plugin.cms:CONTENT_WORKFLOW_DESCRIPTION_END 153 * - editing-description : i18nkey for description when the content is beeing edited : plugin.cms:CONTENT_WORKFLOW_EDITING_DESCRIPTION 154 * So there is no need to define a <class> by default or import js files. 155 * Additionnal i18nkeys used are plugin.cms:CONTENT_WORKFLOW_DESCRIPTION and plugin.cms:CONTENT_WORKFLOW_LOCKED_DESCRIPTION 156 */ 157public class WorkflowStepsClientSideElement extends StaticFileImportsClientSideElement implements MenuClientSideElement 158{ 159 /** The client side element component manager for menu items. */ 160 protected ThreadSafeComponentManager<ClientSideElement> _menuItemManager; 161 /** The service manager */ 162 protected ServiceManager _smanager; 163 /** Runtime users manager */ 164 protected UserManager _userManager; 165 /** Workflow provider */ 166 protected WorkflowProvider _workflowProvider; 167 /** Workflow helper */ 168 protected WorkflowHelper _workflowHelper; 169 /** Ametys object resolver */ 170 protected AmetysObjectResolver _resolver; 171 /** The content helper */ 172 protected ContentHelper _contentHelper; 173 /** The referenced client side element */ 174 protected List<ClientSideElement> _referencedClientSideElement; 175 /** The menu items */ 176 protected Map<String, List<ClientSideElement>> _menuItems; 177 /** The unresolved items */ 178 protected Map<String, List<String>> _unresolvedMenuItems; 179 /** The scripts */ 180 protected List<Script> _scripts; 181 182 @Override 183 public void service(ServiceManager manager) throws ServiceException 184 { 185 super.service(manager); 186 _smanager = manager; 187 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 188 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 189 _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE); 190 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 191 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 192 } 193 194 @Override 195 public void configure(Configuration configuration) throws ConfigurationException 196 { 197 _menuItemManager = new ThreadSafeComponentManager<>(); 198 _menuItemManager.setLogger(LoggerFactory.getLogger("cms.plugin.threadsafecomponent")); 199 _menuItemManager.service(_smanager); 200 201 // Menu items 202 _referencedClientSideElement = new ArrayList<>(); 203 _unresolvedMenuItems = new HashMap<>(); 204 _menuItems = new HashMap<>(); 205 206 super.configure(configuration); 207 208 _configureWorkflow(configuration); 209 } 210 211 /** 212 * Read the workflow configuration, to set up the scripts. 213 * @param configuration The configuration 214 * @throws ConfigurationException If an error occurs 215 */ 216 protected void _configureWorkflow(Configuration configuration) throws ConfigurationException 217 { 218 Configuration workflowConfiguration = configuration.getChild("workflow", false); 219 if (workflowConfiguration != null) 220 { 221 String workflowName = workflowConfiguration.getAttribute("name"); 222 223 Configuration actionsConfiguration = workflowConfiguration.getChild("workflow-actions", true); 224 List<Integer> actions = new ArrayList<>(); 225 boolean actionsIncludeMode = "include".equals(actionsConfiguration.getAttribute("mode", "exclude")); 226 for (Configuration action : actionsConfiguration.getChildren("action")) 227 { 228 actions.add(action.getValueAsInteger()); 229 } 230 231 Configuration stepsConfiguration = workflowConfiguration.getChild("workflow-steps", true); 232 List<Integer> steps = new ArrayList<>(); 233 boolean stepsIncludeMode = "include".equals(stepsConfiguration.getAttribute("mode", "exclude")); 234 for (Configuration action : stepsConfiguration.getChildren("step")) 235 { 236 steps.add(action.getValueAsInteger()); 237 } 238 239 Map<Integer, Configuration> specificStepConfig = new HashMap<>(); 240 for (Configuration stepConfiguration : workflowConfiguration.getChild("steps", true).getChildren("step")) 241 { 242 Integer id = stepConfiguration.getAttributeAsInteger("id"); 243 specificStepConfig.put(id, stepConfiguration); 244 } 245 246 WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName); 247 248 if (workflowDescriptor == null) 249 { 250 throw new ConfigurationException("Unknown workflow name '" + workflowName + "' specified in configuration", workflowConfiguration); 251 } 252 253 List<Integer> allowedStepIds = _getAllowedSteps(workflowDescriptor, stepsIncludeMode, steps); 254 List<Integer> allowedActionIds = _getAllowedActions(workflowDescriptor, allowedStepIds, actionsIncludeMode, actions); 255 256 _configureScripts(workflowConfiguration, allowedStepIds, specificStepConfig, allowedActionIds, workflowDescriptor); 257 } 258 } 259 260 /** 261 * Get the steps allowed for the current workflow 262 * @param workflowDescriptor The workflow descriptor 263 * @param stepsIncludeMode True if the step listed should be the only steps included, false if they should be excluded from all available workflow's steps. 264 * @param configuredSteps A list of step ids. 265 * @return The steps allowed. 266 */ 267 protected List<Integer> _getAllowedSteps(WorkflowDescriptor workflowDescriptor, boolean stepsIncludeMode, List<Integer> configuredSteps) 268 { 269 List<Integer> workflowStepIds = workflowDescriptor.getSteps().stream() 270 .mapToInt(step -> ((StepDescriptor) step).getId()) 271 .boxed() 272 .collect(Collectors.toList()); 273 274 if (stepsIncludeMode) 275 { 276 List<Integer> stepsToRemove = new ArrayList<>(); 277 for (Integer stepId : configuredSteps) 278 { 279 if (!workflowStepIds.contains(stepId)) 280 { 281 getLogger().warn("Unknown step id '" + stepId + "' for workflow '" + workflowDescriptor.getName() + "', in workflow-steps configuration of extension '" + this.getId() + "', it will be ignored"); 282 stepsToRemove.add(stepId); 283 } 284 } 285 configuredSteps.removeAll(stepsToRemove); 286 287 return configuredSteps; 288 } 289 else 290 { 291 List<Integer> unknownStepIds = new ArrayList<>(configuredSteps); 292 unknownStepIds.removeAll(workflowStepIds); 293 for (Integer stepId : unknownStepIds) 294 { 295 getLogger().warn("Unknown step id '" + stepId + "' for workflow '" + workflowDescriptor.getName() + "', in workflow-steps configuration of extension '" + this.getId() + "', it will be ignored"); 296 } 297 298 workflowStepIds.removeAll(configuredSteps); 299 return workflowStepIds; 300 } 301 } 302 303 /** 304 * Get the actions allowed for the current workflow and only for the specified steps. 305 * @param workflowDescriptor The workflow descriptor 306 * @param stepIds The list of steps specified 307 * @param actionsIncludeMode True if the action listed should be the only actions included, false if they should be excluded from all available workflow's actions. 308 * @param configuredActions A list of actions ids. 309 * @return The actions allowed. 310 */ 311 @SuppressWarnings("unchecked") 312 protected List<Integer> _getAllowedActions(WorkflowDescriptor workflowDescriptor, List<Integer> stepIds, boolean actionsIncludeMode, List<Integer> configuredActions) 313 { 314 if (actionsIncludeMode) 315 { 316 return configuredActions; 317 } 318 else 319 { 320 // get the list of action ids for the allowed steps. 321 List<Integer> workflowActionIds = stepIds.stream() 322 .map(stepId -> workflowDescriptor.getStep(stepId).getActions()) 323 .flatMap(actionsLists -> ((List<ActionDescriptor>) actionsLists).stream()) 324 .distinct() 325 .mapToInt(ActionDescriptor::getId) 326 .boxed() 327 .collect(Collectors.toList()); 328 329 workflowActionIds.removeAll(configuredActions); 330 return workflowActionIds; 331 } 332 } 333 334 /** 335 * Configure the list of Scripts, for each step available to the workflow. 336 * @param workflowConfiguration The configuration 337 * @param stepIds The list of steps 338 * @param stepsConfiguration The parameters for each step 339 * @param allowedActionIds The list of globally allowed actions for this workflow 340 * @param workflowDescriptor The descriptor for the current workflow 341 * @throws ConfigurationException If an error occurs 342 */ 343 protected void _configureScripts(Configuration workflowConfiguration, List<Integer> stepIds, Map<Integer, Configuration> stepsConfiguration, List<Integer> allowedActionIds, WorkflowDescriptor workflowDescriptor) throws ConfigurationException 344 { 345 _scripts = new ArrayList<>(); 346 347 Map<String, Object> defaultParameters = null; 348 String defaultClassName = ""; 349 350 Configuration actionConfiguration = workflowConfiguration.getChild("action", true); 351 defaultClassName = actionConfiguration.getAttribute("name", _getDefaultMenuClassName()); 352 defaultParameters = ConfigurationHelper.parsePluginParameters(actionConfiguration, getPluginName(), getLogger()); 353 354 for (Integer stepId : stepIds) 355 { 356 Map<String, Object> parameters; 357 String className; 358 359 Configuration stepConfiguration = null; 360 361 if (stepsConfiguration.containsKey(stepId)) 362 { 363 stepConfiguration = stepsConfiguration.get(stepId); 364 parameters = ConfigurationHelper.parsePluginParameters(stepConfiguration, getPluginName(), getLogger()); 365 className = stepConfiguration.getAttribute("name", defaultClassName); 366 } 367 else 368 { 369 parameters = new HashMap<>(); 370 parameters.putAll(defaultParameters); 371 className = defaultClassName; 372 } 373 374 _configureWorkflowStep(workflowDescriptor, stepId, parameters, stepConfiguration, allowedActionIds); 375 _configureParameters(parameters); 376 377 List<ScriptFile> scriptFiles = new ArrayList<>(_script.getScriptFiles()); 378 List<ScriptFile> cssFiles = new ArrayList<>(_script.getCSSFiles()); 379 Script script = new Script(this.getId() + "-" + stepId, this.getId(), className, scriptFiles, cssFiles, parameters); 380 381 _scripts.add(script); 382 383 if (stepConfiguration != null && stepConfiguration.getChild("action", false) != null) 384 { 385 _configureMenuItems(script, stepConfiguration.getChild("action", false)); 386 } 387 else 388 { 389 _configureMenuItems(script, actionConfiguration); 390 } 391 } 392 } 393 394 /** 395 * Get the default class name for workflow menu 396 * @return the default class name 397 */ 398 protected String _getDefaultMenuClassName() 399 { 400 return "Ametys.plugins.cms.content.controller.WorkflowMenu"; 401 } 402 403 /** 404 * Get the default class name for workflow action 405 * @return the default class name 406 */ 407 protected String _getDefaultActionClassName() 408 { 409 return "Ametys.plugins.cms.content.actions.WorkflowAction.doAction"; 410 } 411 412 /** 413 * Configure the parameters specific to the workflow, for the given step 414 * @param workflowDescriptor The descriptor of the workflow 415 * @param stepId The step 416 * @param stepParameters The parameters of the step 417 * @param stepConfiguration The step configuration 418 * @param allowedActionIds The list of globally allowed actions 419 * @throws ConfigurationException If an error occurs 420 */ 421 protected void _configureWorkflowStep(WorkflowDescriptor workflowDescriptor, Integer stepId, Map<String, Object> stepParameters, Configuration stepConfiguration, List<Integer> allowedActionIds) throws ConfigurationException 422 { 423 stepParameters.put("workflow-name", workflowDescriptor.getName()); 424 stepParameters.put("workflow-step", stepId); 425 stepParameters.put("workflow-step-name", _workflowHelper.getStepName(workflowDescriptor.getName(), stepId)); 426 427 List<Integer> currentStepActions = workflowDescriptor.getStep(stepId) 428 .getActions() 429 .stream() 430 .mapToInt(action -> ((ActionDescriptor) action).getId()) 431 .boxed() 432 .collect(Collectors.toList()); 433 List<Integer> allowedStepActions = ListUtils.intersection(currentStepActions, allowedActionIds); 434 435 List<Integer> stepActions = new ArrayList<>(); 436 List<Integer> commentIds = new ArrayList<>(); 437 438 if (stepConfiguration != null) 439 { 440 Configuration stepWorkflowActions = stepConfiguration.getChild("workflow-actions", false); 441 if (stepWorkflowActions != null) 442 { 443 stepActions.addAll(_configureWorkflowStepActions(workflowDescriptor.getName(), stepId, currentStepActions, allowedStepActions, stepWorkflowActions)); 444 } 445 else 446 { 447 stepActions.addAll(allowedStepActions); 448 } 449 450 Configuration stepWorkflowComments = stepConfiguration.getChild("comments", false); 451 if (stepWorkflowComments != null) 452 { 453 commentIds.addAll(_configureWorkflowStepActions(workflowDescriptor.getName(), stepId, currentStepActions, allowedStepActions, stepWorkflowComments)); 454 } 455 else 456 { 457 commentIds.addAll(allowedStepActions); 458 } 459 } 460 else 461 { 462 stepActions.addAll(allowedStepActions); 463 commentIds.addAll(allowedStepActions); 464 } 465 466 stepParameters.put("workflow-actions-ids", stepActions); 467 stepParameters.put("workflow-comments-ids", commentIds); 468 } 469 470 /** 471 * Get the list of actions available for a step, from the configuration 472 * @param workflowName The name of the current workflow 473 * @param stepId The step 474 * @param currentStepActions All the actions available for this step 475 * @param allowedStepActions The actions allowed by the configuration for this step 476 * @param stepActionsConfiguration The configuration for the step actions 477 * @return The list of actions for the step 478 * @throws ConfigurationException If an error occurs 479 */ 480 protected List<Integer> _configureWorkflowStepActions(String workflowName, Integer stepId, List<Integer> currentStepActions, List<Integer> allowedStepActions, Configuration stepActionsConfiguration) throws ConfigurationException 481 { 482 List<Integer> stepActions = new ArrayList<>(); 483 484 boolean actionsIncludeMode = "include".equals(stepActionsConfiguration.getAttribute("mode", "exclude")); 485 if (actionsIncludeMode) 486 { 487 // include mode, add all listed action if they are available for the current step. 488 for (Configuration workflowAction : stepActionsConfiguration.getChildren("action")) 489 { 490 int action = workflowAction.getValueAsInteger(); 491 if (currentStepActions.contains(action)) 492 { 493 stepActions.add(action); 494 } 495 else 496 { 497 getLogger().warn("Unknown action id '" + action + "' for step '" + stepId + "', for workflow '" 498 + workflowName + "', in steps configuration of extension '" + this.getId() + "', it will be ignored"); 499 } 500 } 501 } 502 else 503 { 504 // exclude mode, add all allowed step actions but the ones listed 505 stepActions.addAll(allowedStepActions); 506 for (Configuration workflowAction : stepActionsConfiguration.getChildren("action")) 507 { 508 Integer action = workflowAction.getValueAsInteger(); 509 if (allowedStepActions.contains(action)) 510 { 511 stepActions.remove(action); 512 } 513 else 514 { 515 getLogger().warn("Unknown action id '" + action + "' for step '" + stepId + "', for workflow '" 516 + workflowName + "', in steps configuration of extension '" + this.getId() + "', it will be ignored"); 517 } 518 } 519 } 520 521 return stepActions; 522 } 523 524 @Override 525 public List<ClientSideElement> getReferencedClientSideElements() 526 { 527 return _referencedClientSideElement; 528 } 529 530 @Override 531 public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters) 532 { 533 try 534 { 535 _resolveMenuItems(); 536 } 537 catch (Exception e) 538 { 539 throw new IllegalStateException("Unable to lookup client side element local components", e); 540 } 541 542 for (Script script : _scripts) 543 { 544 Map<String, Object> parameters = script.getParameters(); 545 546 if (_menuItems.containsKey(script.getId())) 547 { 548 List<String> menuItems = new ArrayList<>(); 549 for (ClientSideElement element : _menuItems.get(script.getId())) 550 { 551 menuItems.add(element.getId()); 552 } 553 parameters.put("menu-items", menuItems); 554 } 555 } 556 557 return _scripts; 558 } 559 560 @Override 561 protected Script _configureScript(Configuration configuration) throws ConfigurationException 562 { 563 Script script = super._configureScript(configuration); 564 List<ScriptFile> scriptFiles = new ArrayList<>(); 565 scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/controller/WorkflowMenu.js")); 566 scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/actions/WorkflowAction.js")); 567 script.getScriptFiles().addAll(scriptFiles); 568 569 return script; 570 } 571 572 /** 573 * Configure parameters recursively 574 * @param parameters The parameters map to fill 575 * @throws ConfigurationException The configuration is incorrect 576 */ 577 protected void _configureParameters(Map<String, Object> parameters) throws ConfigurationException 578 { 579 String workflowStepName = (String) parameters.get("workflow-step-name"); 580 581 // Default label 582 if (parameters.get("label") == null) 583 { 584 parameters.put("label", new I18nizableText("application", workflowStepName)); 585 } 586 587 // Default description 588 if (parameters.get("description") == null) 589 { 590 parameters.put("description", new I18nizableText("application", workflowStepName + "_DESCRIPTION")); 591 } 592 593 // Default footer description 594 if (parameters.get("footerDescription") == null) 595 { 596 parameters.put("footerDescription", new I18nizableText("application", workflowStepName + "_FOOTER")); 597 } 598 599 // Selection target type 600 if (parameters.get("selection-target-id") == null) 601 { 602 parameters.put("selection-target-id", _getSelectionTargetId()); 603 // DO NOT add selection-target-parameter with workflow name : the button is show/hide according the workflow name on client side 604 } 605 606 // Default icons 607 String[] icons = new String[]{"-small", "-medium", "-large"}; 608 for (String icon : icons) 609 { 610 if (parameters.get("icon" + icon) == null) 611 { 612 I18nizableText workflowStepNameText = new I18nizableText("application", workflowStepName); 613 if ("application".equals(workflowStepNameText.getCatalogue())) 614 { 615 parameters.put("icon" + icon, new I18nizableText("/plugins/" + _getDefaultPluginName() + "/resources_workflow/" + workflowStepNameText.getKey() + icon + ".png")); 616 } 617 else 618 { 619 String pluginName = workflowStepNameText.getCatalogue().substring("plugin.".length()); 620 parameters.put("icon" + icon, new I18nizableText("/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepNameText.getKey() + icon + ".png")); 621 } 622 } 623 } 624 625 // Multiselection enabled 626 if (!parameters.containsKey("selection-enable-multiselection")) 627 { 628 parameters.put("selection-enable-multiselection", true); 629 } 630 631 _configureDefaultDescriptions(parameters); 632 } 633 634 /** 635 * Get the default plugin name 636 * @return the default plugin name 637 */ 638 protected String _getDefaultPluginName() 639 { 640 return "cms"; 641 } 642 643 /** 644 * Get the selection target id (can be a Regexp) 645 * @return the selection target id 646 */ 647 protected String _getSelectionTargetId() 648 { 649 return "^content$"; 650 } 651 652 /** 653 * Configure the menu items 654 * @param script The parameters map to fill 655 * @param configuration The items configuration 656 */ 657 @SuppressWarnings("unchecked") 658 protected void _configureMenuItems(Script script, Configuration configuration) 659 { 660 Map<String, Object> parameters = script.getParameters(); 661 for (int actionId : (List<Integer>) parameters.get("workflow-actions-ids")) 662 { 663 String id = script.getId() + ".workflow-action-" + actionId; 664 665 String actionName = _workflowHelper.getActionName((String) parameters.get("workflow-name"), actionId); 666 667 DefaultConfiguration conf = new DefaultConfiguration("extension"); 668 conf.setAttribute("id", id); 669 670 DefaultConfiguration classConf = new DefaultConfiguration("class"); 671 classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController"); 672 673 // Label 674 DefaultConfiguration labelConf = new DefaultConfiguration("label"); 675 labelConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-label").getAttribute("i18n", "true")); 676 labelConf.setValue(configuration.getChild("menu-" + actionId + "-label").getValue(actionName)); 677 classConf.addChild(labelConf); 678 679 // Description 680 DefaultConfiguration descConf = new DefaultConfiguration("description"); 681 descConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-description").getAttribute("i18n", "true")); 682 descConf.setValue(configuration.getChild("menu-" + actionId + "-description").getValue(actionName + "_DESCRIPTION")); 683 classConf.addChild(descConf); 684 685 // Workflow action id 686 DefaultConfiguration wActionConf = new DefaultConfiguration("workflow-action-id"); 687 wActionConf.setValue(actionId); 688 classConf.addChild(wActionConf); 689 690 // Action 691 DefaultConfiguration actionConf = new DefaultConfiguration("action"); 692 actionConf.setValue(configuration.getChild("menu-" + actionId + "-action").getValue(_getDefaultActionClassName())); 693 classConf.addChild(actionConf); 694 695 // Comment 696 DefaultConfiguration commentConf = new DefaultConfiguration("comment"); 697 commentConf.setValue(configuration.getChild("menu-" + actionId + "-comment").getValueAsBoolean(((List<Integer>) parameters.get("workflow-comments-ids")).contains(actionId))); 698 classConf.addChild(commentConf); 699 700 _additionalMenuItemConfiguration(configuration, classConf, actionId, parameters); 701 702 conf.addChild(classConf); 703 704 _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf); 705 if (!_unresolvedMenuItems.containsKey(script.getId())) 706 { 707 _unresolvedMenuItems.put(script.getId(), new ArrayList<>()); 708 } 709 _unresolvedMenuItems.get(script.getId()).add(id); 710 } 711 } 712 713 /** 714 * Additional configuration for menu items 715 * @param itemConf The item configuration 716 * @param classConf The class configuration 717 * @param actionId The workflow action id 718 * @param parameters The script parameters 719 */ 720 protected void _additionalMenuItemConfiguration (Configuration itemConf, DefaultConfiguration classConf, int actionId, Map<String, Object> parameters) 721 { 722 // Nothing to do 723 } 724 725 /** 726 * Configure the default description 727 * @param parameters The parameters 728 */ 729 protected void _configureDefaultDescriptions(Map<String, Object> parameters) 730 { 731 // Empty selection 732 if (!parameters.containsKey("selection-description-empty")) 733 { 734 parameters.put("selection-description-empty", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION")); 735 } 736 737 // No match selection 738 if (!parameters.containsKey("selection-description-nomatch")) 739 { 740 parameters.put("selection-description-nomatch", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMATCH_DESCRIPTION")); 741 } 742 743 // Multiselection forbidden 744 if (!parameters.containsKey("selection-description-multiselectionforbidden")) 745 { 746 parameters.put("selection-description-multiselectionforbidden", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION")); 747 } 748 749 // No available action 750 if (!parameters.containsKey("noaction-available-description")) 751 { 752 parameters.put("noaction-available-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION")); 753 } 754 755 // Refreshing 756 if (!parameters.containsKey("refreshing-description")) 757 { 758 parameters.put("refreshing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_REFRESH_DESCRIPTION")); 759 } 760 761 // Content selected 762 if (!parameters.containsKey("contentselected-start-description")) 763 { 764 parameters.put("contentselected-start-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_BEGIN")); 765 } 766 if (!parameters.containsKey("contentselected-end-description")) 767 { 768 parameters.put("contentselected-end-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_END")); 769 } 770 771 // Editing 772 if (!parameters.containsKey("editing-description")) 773 { 774 parameters.put("editing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_EDITING_DESCRIPTION")); 775 } 776 } 777 778 /** 779 * Get the workflow state of contents 780 * @param contentsId The ids of contents to test workflow status 781 * @param scriptId The script id 782 * @return The workflow state 783 */ 784 @Callable 785 public Map<String, Object> getWorkflowState(List<String> contentsId, String scriptId) 786 { 787 List<Map<String, Object>> contents = new ArrayList<>(); 788 boolean invalidWorkflowForAllContents = true; 789 790 for (String contentId : contentsId) 791 { 792 Content content = _resolver.resolveById(contentId); 793 794 if (content instanceof WorkflowAwareContent) 795 { 796 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 797 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 798 799 for (Script script : _scripts) 800 { 801 if (script.getId().equals(scriptId)) 802 { 803 long wId = waContent.getWorkflowId(); 804 String workflowName = (String) script.getParameters().get("workflow-name"); 805 if (!workflow.getWorkflowName(wId).equals(workflowName)) 806 { 807 continue; 808 } 809 810 invalidWorkflowForAllContents = false; 811 812 List<Step> steps = workflow.getCurrentSteps(wId); 813 List<Integer> stepIds = new ArrayList<>(); 814 for (Step step : steps) 815 { 816 stepIds.add(step.getStepId()); 817 } 818 819 Integer workflowStepId = (Integer) script.getParameters().get("workflow-step"); 820 if (stepIds.contains(workflowStepId)) 821 { 822 Map<String, Object> contentParams = _getContentParameters(content, workflow, script, wId); 823 contents.add(contentParams); 824 } 825 } 826 } 827 } 828 } 829 830 Map<String, Object> results = new HashMap<>(); 831 results.put("contents", contents); 832 833 if (invalidWorkflowForAllContents) 834 { 835 results.put("invalidWorkflow", true); 836 } 837 return results; 838 } 839 840 @SuppressWarnings("unchecked") 841 private Map<String, Object> _getContentParameters(Content content, AmetysObjectWorkflow workflow, Script script, long wId) 842 { 843 Map<String, Object> contentParams = new HashMap<>(); 844 contentParams.put("id", content.getId()); 845 contentParams.put("title", _contentHelper.getTitle(content)); 846 847 String i18nKey = "CONTENT_WORKFLOW_DESCRIPTION"; 848 849 List<String> workflowI18nParameters = new ArrayList<>(); 850 workflowI18nParameters.add(_contentHelper.getTitle(content)); 851 852 boolean isLocked = false; 853 if (content instanceof LockableAmetysObject) 854 { 855 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 856 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser())) 857 { 858 i18nKey = "CONTENT_WORKFLOW_LOCKED_DESCRIPTION"; 859 860 UserIdentity currentLockerIdentity = lockableContent.getLockOwner(); 861 User currentLocker = currentLockerIdentity != null ? _userManager.getUser(currentLockerIdentity.getPopulationId(), currentLockerIdentity.getLogin()) : null; 862 863 workflowI18nParameters.add(currentLocker != null ? currentLocker.getFullName() : ""); 864 workflowI18nParameters.add(currentLockerIdentity != null ? currentLockerIdentity.getLogin() : "Anonymous"); 865 866 isLocked = true; 867 } 868 } 869 870 contentParams.put("description", new I18nizableText("plugin.cms", i18nKey, workflowI18nParameters)); 871 contentParams.put("locked", isLocked); 872 873 Map<String, Object> vars = new HashMap<>(); 874 vars.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 875 vars.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String> ()); 876 877 int[] availableActions = workflow.getAvailableActions(wId, vars); 878 Arrays.sort(availableActions); 879 880 List<Integer> activeActions = new ArrayList<>(); 881 for (int actionId : (List<Integer>) script.getParameters().get("workflow-actions-ids")) 882 { 883 if (Arrays.binarySearch(availableActions, actionId) >= 0) 884 { 885 activeActions.add(actionId); 886 } 887 } 888 contentParams.put("actions", activeActions); 889 890 return contentParams; 891 } 892 893 private void _resolveMenuItems() throws Exception 894 { 895 if (_unresolvedMenuItems != null) 896 { 897 _menuItemManager.initialize(); 898 899 for (String scriptId : _unresolvedMenuItems.keySet()) 900 { 901 for (String id : _unresolvedMenuItems.get(scriptId)) 902 { 903 ClientSideElement element; 904 try 905 { 906 element = _menuItemManager.lookup(id); 907 } 908 catch (ComponentException e) 909 { 910 throw new Exception("Unable to lookup client side element role: '" + id + "'", e); 911 } 912 913 if (!_menuItems.containsKey(scriptId)) 914 { 915 _menuItems.put(scriptId, new ArrayList<>()); 916 } 917 _menuItems.get(scriptId).add(element); 918 _referencedClientSideElement.add(element); 919 } 920 } 921 } 922 923 _unresolvedMenuItems = null; 924 } 925}