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