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            DefaultConfiguration labelConf = new DefaultConfiguration("label");
682            labelConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-label").getAttribute("i18n", "true"));
683            labelConf.setValue(configuration.getChild("menu-" + actionId + "-label").getValue(actionName));
684            classConf.addChild(labelConf);
685            
686            // Description
687            DefaultConfiguration descConf = new DefaultConfiguration("description");
688            descConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-description").getAttribute("i18n", "true"));
689            descConf.setValue(configuration.getChild("menu-" + actionId + "-description").getValue(actionName + "_DESCRIPTION"));
690            classConf.addChild(descConf);
691            
692            // Workflow action id
693            DefaultConfiguration wActionConf = new DefaultConfiguration("workflow-action-id");
694            wActionConf.setValue(actionId);
695            classConf.addChild(wActionConf);
696            
697            // Action
698            DefaultConfiguration actionConf = new DefaultConfiguration("action");
699            actionConf.setValue(configuration.getChild("menu-" + actionId + "-action").getValue(_getDefaultActionClassName()));
700            classConf.addChild(actionConf);
701            
702            // Comment
703            DefaultConfiguration commentConf = new DefaultConfiguration("comment");
704            commentConf.setValue(configuration.getChild("menu-" + actionId + "-comment").getValueAsBoolean(((List<Integer>) parameters.get("workflow-comments-ids")).contains(actionId)));
705            classConf.addChild(commentConf);
706            
707            _additionalMenuItemConfiguration(configuration, classConf, actionId, parameters);
708            
709            conf.addChild(classConf);
710            
711            _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf);
712            if (!_unresolvedMenuItems.containsKey(script.getId()))
713            {
714                _unresolvedMenuItems.put(script.getId(), new ArrayList<>());
715            }
716            _unresolvedMenuItems.get(script.getId()).add(id);
717        }
718    }
719    
720    /**
721     * Additional configuration for menu items
722     * @param itemConf The item configuration
723     * @param classConf The class configuration
724     * @param actionId The workflow action id
725     * @param parameters The script parameters
726     */
727    protected void _additionalMenuItemConfiguration (Configuration itemConf, DefaultConfiguration classConf, int actionId, Map<String, Object> parameters)
728    {
729        // Nothing to do
730    }
731    
732    /**
733     * Configure the default description
734     * @param parameters The parameters
735     */
736    protected void _configureDefaultDescriptions(Map<String, Object> parameters)
737    {
738        // Empty selection
739        if (!parameters.containsKey("selection-description-empty"))
740        {
741            parameters.put("selection-description-empty", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION"));
742        }
743        
744        // No match selection
745        if (!parameters.containsKey("selection-description-nomatch"))
746        {
747            parameters.put("selection-description-nomatch", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMATCH_DESCRIPTION"));
748        }
749        
750        // Multiselection forbidden
751        if (!parameters.containsKey("selection-description-multiselectionforbidden"))
752        {
753            parameters.put("selection-description-multiselectionforbidden", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION"));
754        }
755        
756        // No available action
757        if (!parameters.containsKey("noaction-available-description"))
758        {
759            parameters.put("noaction-available-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION"));
760        }
761        
762        // Refreshing
763        if (!parameters.containsKey("refreshing-description"))
764        {
765            parameters.put("refreshing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_REFRESH_DESCRIPTION"));
766        }
767        
768        // Content selected
769        if (!parameters.containsKey("contentselected-start-description"))
770        {
771            parameters.put("contentselected-start-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_BEGIN"));
772        }
773        if (!parameters.containsKey("contentselected-end-description"))
774        {
775            parameters.put("contentselected-end-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_END"));
776        }
777        
778        // Editing
779        if (!parameters.containsKey("editing-description"))
780        {
781            parameters.put("editing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_EDITING_DESCRIPTION"));
782        }
783    }
784
785    /**
786     * Get the workflow state of contents
787     * @param contentsId The ids of contents to test workflow status
788     * @param scriptId The script id
789     * @return The workflow state
790     */
791    @Callable
792    public Map<String, Object> getWorkflowState(List<String> contentsId, String scriptId)
793    {
794        List<Map<String, Object>> contents = new ArrayList<>();
795        boolean invalidWorkflowForAllContents = true;
796        
797        for (String contentId : contentsId)
798        {
799            Content content = _resolver.resolveById(contentId);
800            
801            if (content instanceof WorkflowAwareContent)
802            {
803                WorkflowAwareContent waContent = (WorkflowAwareContent) content;
804                AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
805                
806                for (Script script : _scripts)
807                {
808                    if (script.getId().equals(scriptId))
809                    {
810                        long wId = waContent.getWorkflowId();
811                        String workflowName = (String) script.getParameters().get("workflow-name");
812                        if (!workflow.getWorkflowName(wId).equals(workflowName))
813                        {
814                            continue;
815                        }
816                        
817                        invalidWorkflowForAllContents = false;
818                        
819                        List<Step> steps = workflow.getCurrentSteps(wId);
820                        List<Integer> stepIds = new ArrayList<>();
821                        for (Step step : steps)
822                        {
823                            stepIds.add(step.getStepId());
824                        }
825                        
826                        Integer workflowStepId = (Integer) script.getParameters().get("workflow-step");
827                        if (stepIds.contains(workflowStepId))
828                        {
829                            Map<String, Object> contentParams = _getContentParameters(content, workflow, script, wId);
830                            contents.add(contentParams);
831                        }
832                    }
833                }
834            }
835        }
836        
837        Map<String, Object> results = new HashMap<>();
838        results.put("contents", contents);
839        
840        if (invalidWorkflowForAllContents)
841        {
842            results.put("invalidWorkflow", true);
843        }
844        return results;
845    }
846
847    @SuppressWarnings("unchecked")
848    private Map<String, Object> _getContentParameters(Content content, AmetysObjectWorkflow workflow, Script script, long wId)
849    {
850        Map<String, Object> contentParams = new HashMap<>();
851        contentParams.put("id", content.getId());
852        contentParams.put("title", _contentHelper.getTitle(content));
853        
854        String i18nKey = "CONTENT_WORKFLOW_DESCRIPTION";
855        
856        List<String> workflowI18nParameters = new ArrayList<>();
857        workflowI18nParameters.add(_contentHelper.getTitle(content));
858        
859        boolean isLocked = false;
860        if (content instanceof LockableAmetysObject)
861        {
862            LockableAmetysObject lockableContent = (LockableAmetysObject) content;
863            if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()))
864            {
865                i18nKey = "CONTENT_WORKFLOW_LOCKED_DESCRIPTION";
866                
867                UserIdentity currentLockerIdentity = lockableContent.getLockOwner();
868                User currentLocker = currentLockerIdentity != null ? _userManager.getUser(currentLockerIdentity.getPopulationId(), currentLockerIdentity.getLogin()) : null;
869   
870                workflowI18nParameters.add(currentLocker != null ? currentLocker.getFullName() : "");
871                workflowI18nParameters.add(currentLockerIdentity != null ? currentLockerIdentity.getLogin() : "Anonymous");
872                
873                isLocked = true;
874            }
875        }
876        
877        contentParams.put("description", new I18nizableText("plugin.cms", i18nKey, workflowI18nParameters));
878        contentParams.put("locked", isLocked);
879   
880        Map<String, Object> vars = new HashMap<>();
881        vars.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
882        vars.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String> ());
883   
884        int[] availableActions = workflow.getAvailableActions(wId, vars);
885        Arrays.sort(availableActions);
886        
887        List<Integer> activeActions = new ArrayList<>();
888        for (int actionId : (List<Integer>) script.getParameters().get("workflow-actions-ids"))
889        {
890            if (Arrays.binarySearch(availableActions, actionId) >= 0)
891            {
892                activeActions.add(actionId);
893            }
894        }
895        contentParams.put("actions", activeActions);
896        
897        return contentParams;
898    }
899    
900    private void _resolveMenuItems() throws Exception
901    {
902        if (_unresolvedMenuItems != null)
903        {
904            _menuItemManager.initialize();
905            
906            for (String scriptId : _unresolvedMenuItems.keySet())
907            {
908                for (String id : _unresolvedMenuItems.get(scriptId))
909                {
910                    ClientSideElement element;
911                    try
912                    {
913                        element = _menuItemManager.lookup(id);
914                    }
915                    catch (ComponentException e)
916                    {
917                        throw new Exception("Unable to lookup client side element role: '" + id + "'", e);
918                    }
919                    
920                    if (!_menuItems.containsKey(scriptId))
921                    {
922                        _menuItems.put(scriptId, new ArrayList<>());
923                    }
924                    _menuItems.get(scriptId).add(element);
925                    _referencedClientSideElement.add(element);
926                }
927            }
928        }
929        
930        _unresolvedMenuItems = null;
931    }
932}