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