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.cms.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(Map<String, Object> contextParameters)
526    {
527        if (hasRight(getRights(contextParameters)))
528        {
529            return _referencedClientSideElement;
530        }
531        else
532        {
533            return List.of();
534        }
535    }
536    
537    @Override
538    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
539    {
540        try
541        {
542            _resolveMenuItems();
543        }
544        catch (Exception e)
545        {
546            throw new IllegalStateException("Unable to lookup client side element local components", e);
547        }
548
549        for (Script script : _scripts)
550        {
551            Map<String, Object> parameters = script.getParameters();
552            
553            if (_menuItems.containsKey(script.getId()))
554            {
555                List<String> menuItems = new ArrayList<>();
556                for (ClientSideElement element : _menuItems.get(script.getId()))
557                {
558                    menuItems.add(element.getId());
559                }
560                parameters.put("menu-items", menuItems);
561            }
562        }
563        
564        return _scripts;
565    }
566    
567    @Override
568    protected Script _configureScript(Configuration configuration) throws ConfigurationException
569    {
570        Script script = super._configureScript(configuration);
571        List<ScriptFile> scriptFiles = new ArrayList<>();
572        scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/controller/WorkflowMenu.js"));
573        scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/actions/WorkflowAction.js"));
574        script.getScriptFiles().addAll(scriptFiles);
575
576        return script;
577    }
578
579    /**
580     * Configure parameters recursively 
581     * @param parameters The parameters map to fill
582     * @throws ConfigurationException The configuration is incorrect
583     */
584    protected void _configureParameters(Map<String, Object> parameters) throws ConfigurationException
585    {
586        String workflowStepName = (String) parameters.get("workflow-step-name");
587        
588        // Default label
589        if (parameters.get("label") == null)
590        {
591            parameters.put("label", new I18nizableText("application", workflowStepName));
592        }
593        
594        // Default description
595        if (parameters.get("description") == null)
596        {
597            parameters.put("description", new I18nizableText("application", workflowStepName + "_DESCRIPTION"));
598        }
599        
600        // Default footer description
601        if (parameters.get("footerDescription") == null)
602        {
603            parameters.put("footerDescription", new I18nizableText("application", workflowStepName + "_FOOTER"));
604        }
605        
606        // Selection target type
607        if (parameters.get("selection-target-id") == null)
608        {
609            parameters.put("selection-target-id", _getSelectionTargetId());
610            // DO NOT add selection-target-parameter with workflow name : the button is show/hide according the workflow name on client side
611        }
612        
613        // Default icons
614        String[] icons = new String[]{"-small", "-medium", "-large"};
615        for (String icon : icons)
616        {
617            if (parameters.get("icon" + icon) == null)
618            {
619                I18nizableText workflowStepNameText = new I18nizableText("application", workflowStepName);
620                if ("application".equals(workflowStepNameText.getCatalogue()))
621                {
622                    parameters.put("icon" + icon, new I18nizableText("/plugins/" + _getDefaultPluginName() + "/resources_workflow/" + workflowStepNameText.getKey() + icon + ".png"));
623                }
624                else
625                {
626                    String pluginName = workflowStepNameText.getCatalogue().substring("plugin.".length());
627                    parameters.put("icon" + icon, new I18nizableText("/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepNameText.getKey() + icon + ".png"));
628                }
629            }
630        }
631        
632        // Multiselection enabled
633        if (!parameters.containsKey("selection-enable-multiselection"))
634        {
635            parameters.put("selection-enable-multiselection", true);
636        }
637        
638        _configureDefaultDescriptions(parameters);
639    }
640    
641    /**
642     * Get the default plugin name
643     * @return the default plugin name
644     */
645    protected String _getDefaultPluginName()
646    {
647        return "cms";
648    }
649    
650    /**
651     * Get the selection target id (can be a Regexp)
652     * @return the selection target id
653     */
654    protected String _getSelectionTargetId()
655    {
656        return "^content$";
657    }
658    
659    /**
660     * Configure the menu items
661     * @param script The parameters map to fill
662     * @param configuration The items configuration
663     */
664    @SuppressWarnings("unchecked")
665    protected void _configureMenuItems(Script script, Configuration configuration)
666    {
667        Map<String, Object> parameters = script.getParameters();
668        for (int actionId : (List<Integer>) parameters.get("workflow-actions-ids"))
669        {
670            String id = script.getId() + ".workflow-action-" + actionId;
671            
672            String actionName = _workflowHelper.getActionName((String) parameters.get("workflow-name"), actionId);
673            
674            DefaultConfiguration conf = new DefaultConfiguration("extension");
675            conf.setAttribute("id", id);
676            
677            DefaultConfiguration classConf = new DefaultConfiguration("class");
678            classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController");
679            
680            // Label
681            _addActionLabelConfiguration(configuration, classConf, actionId, actionName);
682            
683            // Description
684            _addActionDescriptionConfiguration(configuration, classConf, actionId, actionName);
685            
686            // Workflow action id
687            DefaultConfiguration wActionConf = new DefaultConfiguration("workflow-action-id");
688            wActionConf.setValue(actionId);
689            classConf.addChild(wActionConf);
690            
691            // Action
692            DefaultConfiguration actionConf = new DefaultConfiguration("action");
693            actionConf.setValue(configuration.getChild("menu-" + actionId + "-action").getValue(_getDefaultActionClassName()));
694            classConf.addChild(actionConf);
695            
696            // Comment
697            _addActionCommentConfiguration(configuration, classConf, actionId, actionName, parameters);
698            
699            _additionalMenuItemConfiguration(configuration, classConf, actionId, parameters);
700            
701            conf.addChild(classConf);
702            
703            _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
704            if (!_unresolvedMenuItems.containsKey(script.getId()))
705            {
706                _unresolvedMenuItems.put(script.getId(), new ArrayList<>());
707            }
708            _unresolvedMenuItems.get(script.getId()).add(id);
709        }
710    }
711    
712    /**
713     * Add the action label configuration
714     * @param configuration the parent configuration
715     * @param classConf the class configuration
716     * @param actionId the action id
717     * @param actionName the action name
718     */
719    protected void _addActionLabelConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName)
720    {
721        DefaultConfiguration labelConf = new DefaultConfiguration("label");
722        labelConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-label").getAttribute("i18n", "true"));
723        labelConf.setValue(configuration.getChild("menu-" + actionId + "-label").getValue(actionName));
724        classConf.addChild(labelConf);
725    }
726    
727    /**
728     * Add the action description configuration
729     * @param configuration the parent configuration
730     * @param classConf the class configuration
731     * @param actionId the action id
732     * @param actionName the action name
733     */
734    protected void _addActionDescriptionConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName)
735    {
736        DefaultConfiguration descConf = new DefaultConfiguration("description");
737        descConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-description").getAttribute("i18n", "true"));
738        descConf.setValue(configuration.getChild("menu-" + actionId + "-description").getValue(actionName + "_DESCRIPTION"));
739        classConf.addChild(descConf);
740    }
741    
742    /**
743     * Add the action comment configuration
744     * @param configuration the parent configuration
745     * @param classConf the class configuration
746     * @param actionId the action id
747     * @param actionName the action name
748     * @param parameters the script parameters
749     */
750    @SuppressWarnings("unchecked")
751    protected void _addActionCommentConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName, Map<String, Object> parameters)
752    {
753        DefaultConfiguration commentConf = new DefaultConfiguration("comment");
754        commentConf.setValue(configuration.getChild("menu-" + actionId + "-comment").getValueAsBoolean(((List<Integer>) parameters.get("workflow-comments-ids")).contains(actionId)));
755        classConf.addChild(commentConf);
756    }
757    
758    /**
759     * Additional configuration for menu items
760     * @param itemConf The item configuration
761     * @param classConf The class configuration
762     * @param actionId The workflow action id
763     * @param parameters The script parameters
764     */
765    protected void _additionalMenuItemConfiguration (Configuration itemConf, DefaultConfiguration classConf, int actionId, Map<String, Object> parameters)
766    {
767        // Nothing to do
768    }
769    
770    /**
771     * Configure the default description
772     * @param parameters The parameters
773     */
774    protected void _configureDefaultDescriptions(Map<String, Object> parameters)
775    {
776        // Empty selection
777        if (!parameters.containsKey("selection-description-empty"))
778        {
779            parameters.put("selection-description-empty", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION"));
780        }
781        
782        // No match selection
783        if (!parameters.containsKey("selection-description-nomatch"))
784        {
785            parameters.put("selection-description-nomatch", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMATCH_DESCRIPTION"));
786        }
787        
788        // Multiselection forbidden
789        if (!parameters.containsKey("selection-description-multiselectionforbidden"))
790        {
791            parameters.put("selection-description-multiselectionforbidden", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION"));
792        }
793        
794        // No available action
795        if (!parameters.containsKey("noaction-available-description"))
796        {
797            parameters.put("noaction-available-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION"));
798        }
799        
800        // Refreshing
801        if (!parameters.containsKey("refreshing-description"))
802        {
803            parameters.put("refreshing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_REFRESH_DESCRIPTION"));
804        }
805        
806        // Content selected
807        if (!parameters.containsKey("contentselected-start-description"))
808        {
809            parameters.put("contentselected-start-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_BEGIN"));
810        }
811        if (!parameters.containsKey("contentselected-end-description"))
812        {
813            parameters.put("contentselected-end-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_END"));
814        }
815        
816        // Editing
817        if (!parameters.containsKey("editing-description"))
818        {
819            parameters.put("editing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_EDITING_DESCRIPTION"));
820        }
821    }
822
823    /**
824     * Get the workflow state of contents
825     * @param contentsId The ids of contents to test workflow status
826     * @param scriptId The script id
827     * @return The workflow state
828     */
829    @Callable
830    public Map<String, Object> getWorkflowState(List<String> contentsId, String scriptId)
831    {
832        List<Map<String, Object>> contents = new ArrayList<>();
833        boolean invalidWorkflowForAllContents = true;
834        
835        for (String contentId : contentsId)
836        {
837            Content content = _resolver.resolveById(contentId);
838            
839            if (content instanceof WorkflowAwareContent)
840            {
841                WorkflowAwareContent waContent = (WorkflowAwareContent) content;
842                AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
843                
844                for (Script script : _scripts)
845                {
846                    if (script.getId().equals(scriptId))
847                    {
848                        long wId = waContent.getWorkflowId();
849                        String workflowName = (String) script.getParameters().get("workflow-name");
850                        if (!workflow.getWorkflowName(wId).equals(workflowName))
851                        {
852                            continue;
853                        }
854                        
855                        invalidWorkflowForAllContents = false;
856                        
857                        List<Step> steps = workflow.getCurrentSteps(wId);
858                        List<Integer> stepIds = new ArrayList<>();
859                        for (Step step : steps)
860                        {
861                            stepIds.add(step.getStepId());
862                        }
863                        
864                        Integer workflowStepId = (Integer) script.getParameters().get("workflow-step");
865                        if (stepIds.contains(workflowStepId))
866                        {
867                            Map<String, Object> contentParams = _getContentParameters(content, workflow, script, wId);
868                            contents.add(contentParams);
869                        }
870                    }
871                }
872            }
873        }
874        
875        Map<String, Object> results = new HashMap<>();
876        results.put("contents", contents);
877        
878        if (invalidWorkflowForAllContents)
879        {
880            results.put("invalidWorkflow", true);
881        }
882        return results;
883    }
884
885    @SuppressWarnings("unchecked")
886    private Map<String, Object> _getContentParameters(Content content, AmetysObjectWorkflow workflow, Script script, long wId)
887    {
888        Map<String, Object> contentParams = new HashMap<>();
889        contentParams.put("id", content.getId());
890        contentParams.put("title", _contentHelper.getTitle(content));
891        
892        String i18nKey = "CONTENT_WORKFLOW_DESCRIPTION";
893        
894        List<String> workflowI18nParameters = new ArrayList<>();
895        workflowI18nParameters.add(_contentHelper.getTitle(content));
896        
897        boolean isLocked = false;
898        if (content instanceof LockableAmetysObject)
899        {
900            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
901            if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()))
902            {
903                i18nKey = "CONTENT_WORKFLOW_LOCKED_DESCRIPTION";
904                
905                UserIdentity currentLockerIdentity = lockableContent.getLockOwner();
906                User currentLocker = currentLockerIdentity != null ? _userManager.getUser(currentLockerIdentity.getPopulationId(), currentLockerIdentity.getLogin()) : null;
907   
908                workflowI18nParameters.add(currentLocker != null ? currentLocker.getFullName() : "");
909                workflowI18nParameters.add(currentLockerIdentity != null ? currentLockerIdentity.getLogin() : "Anonymous");
910                
911                isLocked = true;
912            }
913        }
914        
915        contentParams.put("description", new I18nizableText("plugin.cms", i18nKey, workflowI18nParameters));
916        contentParams.put("locked", isLocked);
917   
918        Map<String, Object> vars = new HashMap<>();
919        vars.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
920        vars.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<> ());
921   
922        int[] availableActions = workflow.getAvailableActions(wId, vars);
923        Arrays.sort(availableActions);
924        
925        List<Integer> activeActions = new ArrayList<>();
926        for (int actionId : (List<Integer>) script.getParameters().get("workflow-actions-ids"))
927        {
928            if (Arrays.binarySearch(availableActions, actionId) >= 0)
929            {
930                activeActions.add(actionId);
931            }
932        }
933        contentParams.put("actions", activeActions);
934        
935        return contentParams;
936    }
937    
938    private void _resolveMenuItems() throws Exception
939    {
940        if (_unresolvedMenuItems != null)
941        {
942            _menuItemManager.initialize();
943            
944            for (String scriptId : _unresolvedMenuItems.keySet())
945            {
946                for (String id : _unresolvedMenuItems.get(scriptId))
947                {
948                    ClientSideElement element;
949                    try
950                    {
951                        element = _menuItemManager.lookup(id);
952                    }
953                    catch (ComponentException e)
954                    {
955                        throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
956                    }
957                    
958                    if (!_menuItems.containsKey(scriptId))
959                    {
960                        _menuItems.put(scriptId, new ArrayList<>());
961                    }
962                    _menuItems.get(scriptId).add(element);
963                    _referencedClientSideElement.add(element);
964                }
965            }
966        }
967        
968        _unresolvedMenuItems = null;
969    }
970}