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