001/*
002 *  Copyright 2017 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.plugins.bpm;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.function.Function;
032import java.util.stream.Collectors;
033
034import org.apache.avalon.framework.activity.Initializable;
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.configuration.Configuration;
037import org.apache.avalon.framework.configuration.ConfigurationException;
038import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
039import org.apache.avalon.framework.context.Context;
040import org.apache.avalon.framework.context.ContextException;
041import org.apache.avalon.framework.context.Contextualizable;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.cocoon.components.ContextHelper;
046import org.apache.cocoon.environment.Request;
047import org.apache.cocoon.servlet.multipart.Part;
048import org.apache.cocoon.servlet.multipart.PartOnDisk;
049import org.apache.commons.lang.StringUtils;
050import org.apache.commons.lang3.ArrayUtils;
051import org.xml.sax.SAXException;
052
053import org.ametys.core.group.GroupIdentity;
054import org.ametys.core.group.GroupManager;
055import org.ametys.core.right.RightManager;
056import org.ametys.core.right.RightManager.RightResult;
057import org.ametys.core.ui.Callable;
058import org.ametys.core.user.CurrentUserProvider;
059import org.ametys.core.user.UserIdentity;
060import org.ametys.core.util.DateUtils;
061import org.ametys.core.util.I18nUtils;
062import org.ametys.core.util.JSONUtils;
063import org.ametys.core.util.URIUtils;
064import org.ametys.plugins.bpm.jcr.JCRWorkflow;
065import org.ametys.plugins.bpm.jcr.JCRWorkflowFactory;
066import org.ametys.plugins.bpm.jcr.JCRWorkflowProcess;
067import org.ametys.plugins.bpm.jcr.JCRWorkflowProcessFactory;
068import org.ametys.plugins.bpm.workflowsdef.RegisterVariable;
069import org.ametys.plugins.core.user.UserHelper;
070import org.ametys.plugins.explorer.resources.ModifiableResource;
071import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
072import org.ametys.plugins.explorer.resources.ResourceCollection;
073import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper;
074import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper.ResourceOperationMode;
075import org.ametys.plugins.repository.AmetysObject;
076import org.ametys.plugins.repository.AmetysObjectIterable;
077import org.ametys.plugins.repository.AmetysObjectIterator;
078import org.ametys.plugins.repository.AmetysObjectResolver;
079import org.ametys.plugins.repository.AmetysRepositoryException;
080import org.ametys.plugins.repository.ModifiableAmetysObject;
081import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
082import org.ametys.plugins.repository.UnknownAmetysObjectException;
083import org.ametys.plugins.repository.jcr.NameHelper;
084import org.ametys.plugins.repository.query.expression.Expression.Operator;
085import org.ametys.plugins.workflow.support.WorkflowHelper;
086import org.ametys.plugins.workflow.support.WorkflowProvider;
087import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
088import org.ametys.runtime.i18n.I18nizableText;
089import org.ametys.runtime.plugin.component.AbstractLogEnabled;
090import org.ametys.runtime.plugin.component.PluginAware;
091import org.ametys.web.URIPrefixHandler;
092import org.ametys.web.repository.page.Page;
093import org.ametys.web.repository.page.PageQueryHelper;
094import org.ametys.web.repository.site.Site;
095import org.ametys.web.repository.site.SiteManager;
096import org.ametys.web.skin.Skin;
097import org.ametys.web.skin.SkinsManager;
098import org.ametys.web.tags.TagExpression;
099
100import com.opensymphony.workflow.Workflow;
101import com.opensymphony.workflow.WorkflowException;
102import com.opensymphony.workflow.loader.StepDescriptor;
103import com.opensymphony.workflow.loader.WorkflowDescriptor;
104import com.opensymphony.workflow.spi.Step;
105import com.opensymphony.workflow.spi.WorkflowEntry;
106
107/**
108 * Manager for retrieving, creation, edition and suppression of workflows, and retrieving information on workflows definitions
109 */
110public class BPMWorkflowManager extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable, PluginAware
111{
112    /** Avalon Role */
113    public static final String ROLE = BPMWorkflowManager.class.getName();
114    
115    /** Right to create a new workflow */
116    public static final String RIGHT_WORKFLOW_CREATE = "BPM_Rights_Workflow_Create";
117    /** Right to edit any workflow */
118    public static final String RIGHT_WORKFLOW_EDIT = "BPM_Rights_Workflow_Edit";
119    /** Right to delete any workflow */
120    public static final String RIGHT_WORKFLOW_DELETE = "BPM_Rights_Workflow_Delete";
121    
122    /** Right to manage processes */
123    public static final String RIGHTS_PROCESS_MANAGE_PROCESSES = "BPM_Rights_Workflow_Manage_Processes";
124    
125    /** Tag for the create process page */
126    public static final String PAGE_TAG_CREATEPROCESS = "CREATE_PROCESS";
127    /** Tag for the process dashboard page */
128    public static final String PAGE_TAG_PROCESS_DASHBOARD = "PROCESS_DASHBOARD";
129    
130    /** The fixed id for the edit workflow action on processus */
131    public static final int WORKFLOW_ACTION_EDIT = 2;
132
133    
134    /** The plugin root node name */
135    public static final String BPM_ROOT_NODE = "bpm";
136    /** The BPM Workflows node name*/
137    public static final String BPMWORKFLOW_ROOT_NODE = "ametys:workflows";
138    /** The processes node name under each workflow */
139    public static final String BPMPROCESSES_ROOT_NODE = "ametys:processes";
140    
141    private static final String _BPM_PROCESS_TEMPLATE = "bpm-process";
142    
143    private AmetysObjectResolver _resolver;
144    private WorkflowProvider _workflowProvider;
145    private CurrentUserProvider _currentUserProvider;
146    private RightManager _rightManager;
147    private JSONUtils _jsonUtils;
148    private UserHelper _userHelper;
149    private AddOrUpdateResourceHelper _addOrUpdateResourceHelper;
150    private WorkflowHelper _workflowHelper;
151
152    private Map<String, Map<String, Object>> _workflowDefinitionsCache;
153    
154    private Context _context;
155
156    private SkinsManager _skinsManager;
157
158    private AmetysObjectResolver _ametysObjectResolver;
159
160    private SiteManager _siteManager;
161
162    private GroupManager _groupManager;
163    
164    private URIPrefixHandler _uriPrefixHandler;
165
166    private String _pluginName;
167
168    private I18nUtils _i18nUtils;
169
170    @Override
171    public void contextualize(Context context) throws ContextException
172    {
173        _context = context;
174    }
175    
176    @Override
177    public void service(ServiceManager manager) throws ServiceException
178    {
179        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
180        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
181        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
182        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
183        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
184        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
185        _addOrUpdateResourceHelper = (AddOrUpdateResourceHelper) manager.lookup(AddOrUpdateResourceHelper.ROLE);
186        _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
187        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
188        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
189        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
190        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
191        _uriPrefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
192        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
193    }
194    
195    @Override
196    public void initialize() throws Exception
197    {
198        _workflowDefinitionsCache = new HashMap<>();
199    }
200    
201    public void setPluginInfo(String pluginName, String featureName, String id)
202    {
203        _pluginName = pluginName;
204    }
205    
206    /**
207     * Get the list of workflow definitions
208     * @return The list of workflow definitions, mapped by name and label
209     */
210    @Callable
211    public Map<String, Object> getWorkflowDefinitions()
212    {
213        Map<String, Object> result = new HashMap<>();
214        String[] workflowNames = _workflowHelper.getWorkflowNames();
215        
216        Map<String, Object> workflowBuffer = new HashMap<> ();
217        List<Map<String, Object>> workflowsList = new ArrayList<> ();
218        
219        for (String workflowName : workflowNames)
220        {
221            if (workflowName.startsWith("bpm-"))
222            {
223                Map<String, Object> workflowDefinitionData = _getWorkflowDefinitionData(workflowName);
224                if (workflowDefinitionData != null)
225                {
226                    workflowBuffer = new HashMap<>();
227                    workflowBuffer.put("value", workflowName);
228                    workflowBuffer.put("label", _workflowHelper.getWorkflowLabel(workflowName));
229                    workflowsList.add(workflowBuffer);
230                }
231            }
232        }
233        
234        result.put("workflowDefinition", workflowsList);
235        result.put("success", true);
236        return result;
237    }
238    
239    /**
240     * Retrieve the value of a variable from a workflow
241     * @param workflowId The workflow id
242     * @param variableName The variable name
243     * @return The value
244     */
245    public Object getWorkflowVariable(String workflowId, String variableName)
246    {
247        JCRWorkflow workflow = _resolver.resolveById(workflowId);
248        return workflow.getVariable(variableName);
249    }
250    
251    /**
252     * Retrieve the list of variables from a workflow definition
253     * @param workflowDefId The workflow definition id
254     * @return The list of variables, with their attributes such as the label, type, multiple, if defined
255     */
256    @Callable
257    public Map<String, Object> getWorkflowDefinitionVariables(String workflowDefId)
258    {
259        Map<String, Object> result = new HashMap<>();
260        Map<String, Object> workflowDefinitionData = _getWorkflowDefinitionData(workflowDefId);
261        boolean success = false;
262        if (workflowDefinitionData != null && workflowDefinitionData.containsKey("variables"))
263        {
264            @SuppressWarnings("unchecked")
265            Map<String, Map<String, String>> variables = (Map<String, Map<String, String>>) workflowDefinitionData.get("variables");
266            if (variables != null)
267            {
268                success = true;
269                result.put("variables", variables);
270            }
271        }
272        result.put("success", success);
273        return result;
274    }
275    
276    /**
277     * Retrieve the list of workflows
278     * @return The list of workflows
279     */
280    @Callable
281    public Map<String, Object> getWorkflows()
282    {
283        Map<String, Object> result = new HashMap<>();
284        List<Map<String, Object>> workflows = new ArrayList<>();
285        
286        try
287        {
288            ModifiableTraversableAmetysObject workflowsRootNode = _getWorkflowsRootNode(false);
289            AmetysObjectIterable<AmetysObject> children = workflowsRootNode.getChildren();
290            for (AmetysObject child : children)
291            {
292                if (child instanceof JCRWorkflow)
293                {
294                    JCRWorkflow workflow = (JCRWorkflow) child;
295                    String workflowDefinition = workflow.getWorkflowDefinition();
296                    Map<String, Object> workflowJson = _workflowToJson(workflow, _getWorkflowDefinitionData(workflowDefinition));
297                    if (workflowJson != null)
298                    {
299                        workflows.add(workflowJson);
300                    }
301                }
302            }
303        }
304        catch (UnknownAmetysObjectException e)
305        {
306            // no workflows root node yet, ignore
307        }
308        
309        result.put("workflows", workflows);
310        return result;
311    }
312    
313    /**
314     * Retrieve the list of workflows availables to the current user, in JSON format
315     * @return The list of workflows
316     */
317    public List<Map<String, Object>> getWorkflowsAvailables()
318    {
319        List<Map<String, Object>> workflows = new ArrayList<>();
320        
321        try
322        {
323            ModifiableTraversableAmetysObject workflowsRootNode = _getWorkflowsRootNode(false);
324            AmetysObjectIterable<AmetysObject> children = workflowsRootNode.getChildren();
325            for (AmetysObject child : children)
326            {
327                if (child instanceof JCRWorkflow)
328                {
329                    JCRWorkflow workflow = (JCRWorkflow) child;
330                    if (isUserAllowedOnWorkflow(workflow))
331                    {
332                        String workflowDefinition = workflow.getWorkflowDefinition();
333                        Map<String, Object> workflowJson = _workflowToJson(workflow, _getWorkflowDefinitionData(workflowDefinition));
334                        if (workflowJson != null)
335                        {
336                            workflows.add(workflowJson);
337                        }
338                    }
339                }
340            }
341        }
342        catch (UnknownAmetysObjectException e)
343        {
344            // no workflows root node yet, ignore
345        }
346        
347        return workflows;
348    }
349    
350    /**
351     * Retrieve the data of a workflow, in JSON format. Contains both the values of the workflow, and the workflow variables definitions
352     * @param workflowId The workflow id
353     * @return The data of a workflow
354     */
355    @Callable
356    public Map<String, Object> getWorkflowData(String workflowId)
357    {
358        Map<String, Object> result = new HashMap<>();
359        
360        JCRWorkflow workflow = _resolver.resolveById(workflowId);
361        String workflowDefinition = workflow.getWorkflowDefinition();
362        Map<String, Object> workflowDefinitionData = _getWorkflowDefinitionData(workflowDefinition);
363        result.put("workflow", _workflowToJson(workflow, workflowDefinitionData));
364        if (workflowDefinitionData != null)
365        {
366            result.put("variables", workflowDefinitionData.get("variables"));
367        }
368        result.put("success", true);
369        return result;
370    }
371    
372    /**
373     * Add a new workflow
374     * @param values The values for the new workflow. Must contains a "name", "workflowDef" and any variables mandatory for the workflowDef
375     * @return The result, with the new workflow id if successful
376     * @throws IllegalAccessException If a user with insufficient rights try to execute this method
377     */
378    @Callable (right = RIGHT_WORKFLOW_CREATE)
379    public Map<String, Object> addWorkflow(Map<String, Object> values) throws IllegalAccessException
380    {
381        ModifiableTraversableAmetysObject workflowsRoot = _getWorkflowsRootNode(true);
382        
383        String workflowDef = (String) values.getOrDefault("workflowDef", null);
384        String name = (String) values.getOrDefault("title", null);
385        
386        if (StringUtils.isEmpty(workflowDef) || StringUtils.isEmpty(name))
387        {
388            throw new IllegalArgumentException("Missing mandatory arguments for creating a new workflow");
389        }
390        
391        String originalName = NameHelper.filterName(name);
392        // Find unique name
393        String uniqueName = originalName;
394        int index = 2;
395        while (workflowsRoot.hasChild(uniqueName))
396        {
397            uniqueName = originalName + "-" + (index++);
398        }
399
400        JCRWorkflow workflow = (JCRWorkflow) workflowsRoot.createChild(uniqueName, JCRWorkflowFactory.WORKFLOW_NODE_TYPE);
401        workflow.setWorkflowDefinition(workflowDef);
402        _setWorkflowValues(workflow, values);
403        
404        workflow.saveChanges();
405        
406        Map<String, Object> result = new HashMap<>();
407        result.put("id", workflow.getId());
408        
409        return result;
410    }
411    
412    /**
413     * Edit a workflow values
414     * @param workflowId The workflow
415     * @param values The values
416     * @return The result, with the workflow id if successful
417     * @throws IllegalAccessException If a user with insufficient rights try to execute this method
418     */
419    @Callable
420    public Map<String, Object> editWorkflow(String workflowId, Map<String, Object> values) throws IllegalAccessException
421    {
422        JCRWorkflow workflow = _resolver.resolveById(workflowId);
423        
424        UserIdentity currentUser = _currentUserProvider.getUser();
425        if ((currentUser == null || !currentUser.equals(workflow.getOwner())) && _rightManager.currentUserHasRight(RIGHT_WORKFLOW_EDIT, null) != RightResult.RIGHT_ALLOW)
426        {
427            throw new IllegalAccessException("User '" + _currentUserProvider.getUser().toString() + "' tried to create a process with insufficient rights");
428        }
429        
430        _setWorkflowValues(workflow, values);
431        if (workflow.needsSave())
432        {
433            workflow.saveChanges();
434        }
435        
436        Map<String, Object> result = new HashMap<>();
437        result.put("id", workflow.getId());
438        
439        return result;
440    }
441    
442    /**
443     * Delete a list of workflow
444     * @param workflowIds The workflows
445     * @return The result
446     */
447    @Callable
448    public Map<String, Object> deleteWorkflow(List<String> workflowIds)
449    {
450        Map<String, Object> result = new HashMap<>();
451        List<JCRWorkflow> workflows = new ArrayList<>();
452        for (String workflowId : workflowIds)
453        {
454            JCRWorkflow workflow = _resolver.resolveById(workflowId);
455            UserIdentity owner = workflow.getOwner();
456            if (owner != null && owner.equals(_currentUserProvider.getUser())
457                || _rightManager.currentUserHasRight(RIGHT_WORKFLOW_DELETE, null) == RightResult.RIGHT_ALLOW)
458            {
459                List<Object> workflowProcessus = getWorkflowProcessus(workflow);
460                if (workflowProcessus.size() == 0)
461                {
462                    workflows.add(workflow);
463                }
464            }
465        }
466        
467        List<String> workflowDeletedIds = new ArrayList<>();
468        for (JCRWorkflow workflow : workflows)
469        {
470            ModifiableAmetysObject parent = (ModifiableAmetysObject) workflow.getParent();
471            workflowDeletedIds.add(workflow.getId());
472            workflow.remove();
473            parent.saveChanges();
474        }
475        result.put("ids", workflowDeletedIds);
476        
477        result.put("success", true);
478        return result;
479    }
480    
481    /**
482     * Retrieve the list of process created from a workflow
483     * @param workflowId The workflow
484     * @return The list of process
485     */
486    @Callable (right = RIGHTS_PROCESS_MANAGE_PROCESSES)
487    public Map<String, Object> getWorkflowProcesses(String workflowId)
488    {
489        Map<String, Object> result = new HashMap<>();
490        JCRWorkflow workflow = _resolver.resolveById(workflowId);
491        result.put("processes", getWorkflowProcessus(workflow).stream().map(process -> _processToJson((JCRWorkflowProcess) process)).collect(Collectors.toList()));
492        return result;
493    }
494    
495    /**
496     * Retrieve the list of process created from a workflow
497     * @param workflow The workflow
498     * @return The list of process
499     */
500    public List<Object> getWorkflowProcessus(JCRWorkflow workflow)
501    {
502        try
503        {
504            ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, false);
505            return processesRoot.getChildren().stream().collect(Collectors.toList());
506        }
507        catch (UnknownAmetysObjectException e)
508        {
509            // no processes for the workflow
510        }
511        return new ArrayList<>();
512    }
513    
514    
515    /**
516     * Create a new process
517     * @param workflowId The workflow used by the process
518     * @param title The process title
519     * @param site The site on which the process was created
520     * @param description The process description
521     * @param uploadedAttachments The process attachments
522     * @return The new process
523     * @throws WorkflowException If an error occurred creating the workflow
524     * @throws IllegalAccessException If the user is not allowed
525     */
526    public JCRWorkflowProcess createProcess(String workflowId, String title, String site, String description, List<PartOnDisk> uploadedAttachments) throws WorkflowException, IllegalAccessException
527    {
528        JCRWorkflow workflow = _resolver.resolveById(workflowId);
529        
530        if (!isUserAllowedOnWorkflow(workflow))
531        {
532            throw new IllegalAccessException("User '" + _currentUserProvider.getUser().toString() + "' tried to create a process with insufficient rights");
533        }
534        
535        ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, true);
536        
537        String originalName = NameHelper.filterName(title);
538        // Find unique name
539        String uniqueName = originalName;
540        int index = 2;
541        while (processesRoot.hasChild(uniqueName))
542        {
543            uniqueName = originalName + "-" + (index++);
544        }
545
546        JCRWorkflowProcess process = (JCRWorkflowProcess) processesRoot.createChild(uniqueName, JCRWorkflowProcessFactory.WORKFLOW_PROCESS_NODE_TYPE);
547        process.setCreator(_currentUserProvider.getUser());
548        process.setWorkflow(workflowId);
549        process.setCreationDate(new Date());
550        _setProcessValues(process, title, site, description, uploadedAttachments);
551        
552        // create workflow with entry id
553        Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
554        
555        String workflowName = workflow.getWorkflowDefinition();
556        int initialActionId = _workflowHelper.getInitialAction(workflowName); 
557        
558        // Add the workflow used by the process in the request, to be used by any RegisterVariable of the workflow definition 
559        Request request = ContextHelper.getRequest(_context);
560        request.setAttribute("workflowId", process.getWorkflow());
561        
562        Map<String, Object> inputs = new HashMap<>();
563        inputs.put("workflowId", workflowId);
564        inputs.put("process", process);
565        long workflowInstanceId = workflowStore.initialize(workflowName, initialActionId, inputs);
566        process.setWorkflowId(workflowInstanceId);
567        Step currentStep = (Step) workflowStore.getCurrentSteps(process.getWorkflowId()).iterator().next();
568        process.setCurrentStepId(currentStep.getStepId());
569        process.saveChanges();
570        
571        return process;
572    }
573    
574
575    /**
576     * Edit a process
577     * @param processId The process id
578     * @param title The title
579     * @param description The description
580     * @param uploadedAttachments The list of new attachments uploaded
581     * @param attachmentsUntouched The list of untouched attachments names
582     * @throws WorkflowException If an error occurred while executing the workflow action 
583     */
584    public void editProcess(String processId, String title, String description, List<PartOnDisk> uploadedAttachments, List<String> attachmentsUntouched) throws WorkflowException
585    {
586        JCRWorkflowProcess process = _resolver.resolveById(processId);
587        
588        Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
589        Map<String, Object> inputs = new HashMap<>();
590        inputs.put("process", process);
591        
592        inputs.put("title", title);
593        inputs.put("description", description);
594        inputs.put("uploadedAttachments", uploadedAttachments);
595        inputs.put("attachmentsUntouched", attachmentsUntouched);
596        
597        // Add the workflow used by the process in the request, to be used by any RegisterVariable of the workflow definition 
598        Request request = ContextHelper.getRequest(_context);
599        request.setAttribute("workflowId", process.getWorkflow());
600        
601        workflowStore.doAction(process.getWorkflowId(), WORKFLOW_ACTION_EDIT, inputs);
602    }
603    
604    /**
605     * Retrieve a process
606     * @param workflowName The workflow name
607     * @param processName The process name
608     * @return The process
609     */
610    public JCRWorkflowProcess getProcess(String workflowName, String processName)
611    {
612        try
613        {
614            ModifiableTraversableAmetysObject workflowsRoot = _getWorkflowsRootNode(false);
615            JCRWorkflow workflow = workflowsRoot.getChild(workflowName);
616            ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, false);
617            if (processesRoot.hasChild(processName))
618            {
619                return processesRoot.getChild(processName);
620            }
621        }
622        catch (UnknownAmetysObjectException e)
623        {
624            // No workflow or process created with this workflowId and processId
625            return null;
626        }
627        
628        return null;
629    }
630    
631    /**
632     * Determines if the process is complete
633     * @param process The process
634     * @return True if the process is complete
635     */
636    public boolean isComplete(JCRWorkflowProcess process)
637    {
638        AmetysObjectWorkflow aoWorkflow = _workflowProvider.getAmetysObjectWorkflow(process, true);
639        if (aoWorkflow != null)
640        {
641            int entryState = aoWorkflow.getEntryState(process.getWorkflowId());
642            return entryState ==  WorkflowEntry.COMPLETED;
643        }
644        
645        // FIXME on final step the workflow is compelte but deleted ...
646        return false;
647    }
648    
649    /**
650     * Determines if the process can be deleted by the current user
651     * @param process The process
652     * @return True if the process can be deleted
653     */
654    public boolean canDelete(JCRWorkflowProcess process)
655    {
656        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) == RightResult.RIGHT_ALLOW)
657        {
658            // A super admin can delete a process at all time
659            return true;
660        }
661        
662        // Otherwise a process can be deleted only by the creator if is complete
663        return isComplete(process) && process.getCreator().equals(_currentUserProvider.getUser());
664    }
665
666    /**
667     * Delete a process
668     * @param process The process to delete
669     * @throws IllegalAccessException If a user tries to delete a process without being allowed 
670     */
671    public void deleteProcess(JCRWorkflowProcess process) throws IllegalAccessException
672    {
673        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) != RightResult.RIGHT_ALLOW)
674        {
675            Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
676            int entryState = workflowStore.getEntryState(process.getWorkflowId());
677            if (entryState !=  WorkflowEntry.COMPLETED || !process.getCreator().equals(_currentUserProvider.getUser()))
678            {
679                throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete a process without sufficient rights."); 
680            }
681        }
682        
683        ModifiableAmetysObject parent = process.getParent();
684        process.remove();
685        parent.saveChanges();
686    }
687
688    /**
689     * Delete a list of processes
690     * @param processes The processes to delete
691     * @return The result
692     * @throws IllegalAccessException If a user tries to delete a process without being allowed 
693     */
694    @Callable (right = RIGHTS_PROCESS_MANAGE_PROCESSES)
695    public Map<String, Object> deleteProcesses(List<String> processes) throws IllegalAccessException
696    {
697        Map<String, Object> result = new HashMap<>();
698        List<String> processesDeletedIds = new ArrayList<>();
699        for (String processId : processes)
700        {
701            JCRWorkflowProcess process = _resolver.resolveById(processId);
702            ModifiableAmetysObject parent = (ModifiableAmetysObject) process.getParent();
703            processesDeletedIds.add(process.getId());
704            process.remove();
705            parent.saveChanges();
706        }
707
708        result.put("ids", processesDeletedIds);
709        result.put("success", true);
710        return result;
711    }
712    
713    /**
714     * Get the url of a process page
715     * @param process The process
716     * @param siteName The current site name. Can not be null.
717     * @param lang The current language. Can not be null.
718     * @param absolute true to get absolute url.
719     * @return the url of process page
720     */
721    public String getProcessPageUrl(JCRWorkflowProcess process, String siteName, String lang, boolean absolute)
722    {
723        StringBuilder sb = new StringBuilder();
724        
725        if (absolute)
726        {
727            sb.append(_uriPrefixHandler.getAbsoluteUriPrefix(siteName));
728        }
729        else
730        {
731            sb.append(_uriPrefixHandler.getUriPrefix(siteName));
732        }
733        
734        sb.append("/")
735            .append(lang)
736            .append("/_plugins/")
737            .append(_pluginName)
738            .append("/")
739            .append(getTemplateForProcessPage(siteName, lang))
740            .append("/process/");
741        
742        JCRWorkflow workflow = _resolver.resolveById(process.getWorkflow());
743        
744        sb.append(URIUtils.encodePathSegment(workflow.getName()))
745            .append("/")
746            .append(URIUtils.encodePathSegment(process.getName()))
747            .append(".html");
748        
749        return sb.toString();
750    }
751    
752    /**
753     * Get the page with PROCESS_DASHBOARD tag
754     * @param siteName The current site name
755     * @param lang The language
756     * @return The page or null if not found.
757     */
758    public Page getDashboardPage (String siteName, String lang)
759    {
760        TagExpression tagExpression = new TagExpression(Operator.EQ, PAGE_TAG_PROCESS_DASHBOARD);
761        
762        String pathQuery = PageQueryHelper.getPageXPathQuery(siteName, lang, null, tagExpression, null);
763        AmetysObjectIterable<Page> page = _ametysObjectResolver.query(pathQuery);
764        
765        if (page.getSize() == 0)
766        {
767            return null;
768        }
769        
770        return page.iterator().next();
771    }
772    
773    /**
774     * Get the page with CREATE_PROCESS tag
775     * @param siteName The current site name
776     * @param lang The language
777     * @return The page or null if not found.
778     */
779    public Page getCreateProcessPage (String siteName, String lang)
780    {
781        TagExpression tagExpression = new TagExpression(Operator.EQ, PAGE_TAG_CREATEPROCESS);
782        
783        String pathQuery = PageQueryHelper.getPageXPathQuery(siteName, lang, null, tagExpression, null);
784        AmetysObjectIterable<Page> page = _ametysObjectResolver.query(pathQuery);
785        
786        if (page.getSize() == 0)
787        {
788            return null;
789        }
790        
791        return page.iterator().next();
792    }
793    
794    /**
795     * Get the template to use for process rendering
796     * @param siteName The site containing the page
797     * @param lang The sitemap of the page
798     * @return The template to use for process rendering
799     */
800    public String getTemplateForProcessPage(String siteName, String lang)
801    {
802        Site site = _siteManager.getSite(siteName);
803        
804        String skinName = site.getSkinId();
805        Skin skin = _skinsManager.getSkin(skinName);
806        Set<String> templates = skin.getTemplates();
807        
808        // First look for the template bpm-process
809        if (templates.contains(_BPM_PROCESS_TEMPLATE))
810        {
811            return _BPM_PROCESS_TEMPLATE;
812        }
813
814        // Otherwise, returns the template used by create process page
815        Page createProcessPage = getCreateProcessPage(siteName, lang);
816        if (createProcessPage != null)
817        {
818            return createProcessPage.getTemplate();
819        }
820        
821        // Otherwise, fallback to the template page if exists
822        if (templates.contains("page"))
823        {
824            return "page";
825        }
826        
827        // Finally, fallback to the first template
828        return templates.iterator().next();
829    }
830    
831    /**
832     * Test if the current user is present in any variable of the workflow
833     * @param workflow The workflow
834     * @return True if the user is in the workflow variables
835     */
836    @SuppressWarnings("unchecked")
837    public boolean isUserInWorkflowVariables(JCRWorkflow workflow)
838    {
839        Map<String, Object> workflowDefinitionData = _getWorkflowDefinitionData(workflow.getWorkflowDefinition());
840        
841        if (workflowDefinitionData != null && workflowDefinitionData.containsKey("variables"))
842        {
843            Map<String, Map<String, String>> variables = (Map<String, Map<String, String>>) workflowDefinitionData.get("variables");
844            UserIdentity currentUser = _currentUserProvider.getUser();
845            
846            boolean userInVariables = variables.entrySet().stream()
847                .filter(e -> "user".equals(e.getValue().getOrDefault("type", null)))
848                .map(e -> workflow.getVariable(e.getKey()))
849                .filter(var -> var != null && var instanceof String)
850                .map(var -> _jsonUtils.convertJsonToList((String) var))
851                .map(usersList ->
852                {
853                    return usersList.stream()
854                        .map(userData ->
855                        {
856                            Map<String, String> user = (Map<String, String>) userData;
857                            String login = user.get("login");
858                            String populationId = user.get("populationId");
859                            return new UserIdentity(login, populationId);
860                        })
861                        .filter(user -> user != null && user.equals(currentUser))
862                        .findAny();
863                })
864                .filter(optionalUser -> optionalUser.isPresent())
865                .findAny()
866                .isPresent();
867
868            return userInVariables;
869        }
870        return false;
871    }
872
873    /**
874     * Get the list of processes accessibles to the current user
875     * @return The list of processes
876     */
877    public Set<JCRWorkflowProcess> getUserProcesses()
878    {
879        Set<JCRWorkflowProcess> processes = new HashSet<>();
880        UserIdentity user = _currentUserProvider.getUser();
881        if (user == null)
882        {
883            return processes;
884        }
885        
886        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) == RightResult.RIGHT_ALLOW)
887        {
888            // return all processes as we have the right to manage all of them
889            AmetysObjectIterable<JCRWorkflowProcess> processesOwned = _ametysObjectResolver.query("//element(*, ametys:workflowProcess)");
890            Iterator<JCRWorkflowProcess> it = processesOwned.iterator();
891            while (it.hasNext())
892            {
893                processes.add(it.next());
894            }
895            return processes;
896        }
897        
898        // retrieve processes of which we are the creator
899        String xpath = String.format("//element(*, ametys:workflowProcess)[@ametys:creator/ametys:login='%s' and @ametys:creator/ametys:population='%s']", user.getLogin(), user.getPopulationId());
900        AmetysObjectIterable<JCRWorkflowProcess> processesOwned = _ametysObjectResolver.query(xpath);
901        Iterator<JCRWorkflowProcess> it = processesOwned.iterator();
902        while (it.hasNext())
903        {
904            processes.add(it.next());
905        }
906        
907        // add processes for which we are in the workflow variables
908        ModifiableTraversableAmetysObject workflowsRootNode = _getWorkflowsRootNode(false);
909        AmetysObjectIterable<AmetysObject> children = workflowsRootNode.getChildren();
910        for (AmetysObject child : children)
911        {
912            if (child instanceof JCRWorkflow)
913            {
914                JCRWorkflow workflow = (JCRWorkflow) child;
915                try
916                {
917                    ModifiableTraversableAmetysObject processesRootNode = _getProcessesRootNode(workflow, false);
918                    AmetysObjectIterable<JCRWorkflowProcess> workflowChildren = processesRootNode.getChildren();
919                    if (workflowChildren.getSize() > 0 && isUserInWorkflowVariables(workflow))
920                    {
921                        AmetysObjectIterator<JCRWorkflowProcess> it2 = workflowChildren.iterator();
922                        while (it2.hasNext())
923                        {
924                            processes.add(it2.next());
925                        }
926                    }
927                }
928                catch (UnknownAmetysObjectException e)
929                {
930                    // No process for workflow, do nothing
931                }
932            }
933        }
934        
935        return processes;
936    }
937
938    /**
939     * Check if the current user is allowed to create a process from the workflow 
940     * @param workflow The workflow
941     * @return True if the user is allowed
942     */
943    public boolean isUserAllowedOnWorkflow(JCRWorkflow workflow)
944    {
945        UserIdentity user = _currentUserProvider.getUser();
946        if (user != null)
947        {
948            if (user.equals(workflow.getOwner()) || ArrayUtils.contains(workflow.getAllowedUsers(), user))
949            {
950                return true;
951            }
952            
953            GroupIdentity[] allowedGroups = workflow.getAllowedGroups();
954            for (GroupIdentity userGroup : _groupManager.getUserGroups(user))
955            {
956                if (ArrayUtils.contains(allowedGroups, userGroup))
957                {
958                    return true;
959                }
960            }
961        
962        }
963        return false;
964    }
965
966    @SuppressWarnings("unchecked")
967    private void _setWorkflowValues(JCRWorkflow workflow, Map<String, Object> values)
968    {
969        String title = (String) values.get("title");
970        String description = (String) values.getOrDefault("description", null);
971        Map<String, Object> owner = (Map<String, Object>) values.getOrDefault("owner", null);
972        List<Map<String, Object>> allowedUsersValue = (List<Map<String, Object>>) values.getOrDefault("allowedUsers", null);
973        List<Map<String, Object>> allowedGroupsValue = (List<Map<String, Object>>) values.getOrDefault("allowedGroups", null);
974        Map<String, Object> variables = (Map<String, Object>) values.getOrDefault("variables", null);
975
976        workflow.setTitle(title);
977        workflow.setDescription(description);
978        workflow.setOwner(owner != null ? _convertJsonToUser(owner) : null);
979
980        if (allowedUsersValue != null)
981        {
982            Set<UserIdentity> allowedUsers = new HashSet<>();
983            for (Object obj : allowedUsersValue)
984            {
985                UserIdentity user = _convertJsonToUser((Map<String, Object>) obj);
986                if (user != null)
987                {
988                    allowedUsers.add(user);
989                }
990            }
991            
992            workflow.setAllowedUsers(allowedUsers.toArray(new UserIdentity[allowedUsers.size()]));
993        }
994        
995        if (allowedGroupsValue != null)
996        {
997            Set<GroupIdentity> allowedGroups = new HashSet<>();
998            for (Object obj : allowedGroupsValue)
999            {
1000                Map<String, Object> grantedUser = (Map<String, Object>) obj;
1001                String groupId = (String) grantedUser.get("groupId");
1002                String groupDirectory = (String) grantedUser.get("groupDirectory");
1003                
1004                if (groupId != null && groupDirectory != null)
1005                {
1006                    allowedGroups.add(new GroupIdentity(groupId, groupDirectory));
1007                }
1008            }
1009
1010            workflow.setAllowedGroups(allowedGroups.toArray(new GroupIdentity[allowedGroups.size()]));
1011        }
1012        
1013        for (Entry<String, Object> entry : variables.entrySet())
1014        {
1015            workflow.setVariable(entry.getKey(), entry.getValue());
1016        }
1017    }
1018    
1019    private void _setProcessValues(JCRWorkflowProcess process, String title, String site, String description, List<PartOnDisk> uploadedAttachments)
1020    {
1021        if (title != null)
1022        {
1023            process.setTitle(title);
1024        }
1025        
1026        if (site != null)
1027        {
1028            process.setSite(site);
1029        }
1030        
1031        process.setDescription(description);
1032        
1033        ResourceCollection rootAttachments = process.getRootAttachments(true);
1034        if (rootAttachments instanceof ModifiableResourceCollection)
1035        {
1036            _addOrUpdateResourceHelper.checkAddResourceRight((ModifiableResourceCollection) rootAttachments);
1037            
1038            for (Part attachment : uploadedAttachments)
1039            {
1040                _addOrUpdateResourceHelper.performResourceOperation(attachment, (ModifiableResourceCollection) rootAttachments, ResourceOperationMode.ADD);
1041            }
1042        }
1043    }
1044
1045    private UserIdentity _convertJsonToUser(Map<String, Object> user)
1046    {
1047        String login = (String) user.get("login");
1048        String populationId = (String) user.get("populationId");
1049        
1050        if (login != null && populationId != null)
1051        {
1052            return new UserIdentity(login, populationId);
1053        }
1054        
1055        return null;
1056    }
1057    
1058    private Map<String, Object> _workflowToJson(JCRWorkflow workflow, Map<String, Object> workflowDefinitionData)
1059    {
1060        if (workflow == null)
1061        {
1062            return null;
1063        }
1064        
1065        Map<String, Object> jsonData = new HashMap<>();
1066        
1067        jsonData.put("id", workflow.getId());
1068        jsonData.put("title", workflow.getTitle());
1069        Map<String, Object> workflowBuffer = new HashMap<>();
1070        String workflowName = workflow.getWorkflowDefinition();
1071        workflowBuffer.put("value", workflowName);
1072        workflowBuffer.put("label", _workflowHelper.getWorkflowLabel(workflowName));
1073        if (workflowDefinitionData == null)
1074        {
1075            workflowBuffer.put("missing", true);
1076        }
1077        jsonData.put("workflowDefinition", workflowBuffer);
1078        jsonData.put("description", workflow.getDescription());
1079        jsonData.put("owner", _userHelper.user2json(workflow.getOwner(), true));
1080        jsonData.put("allowedUsers", Arrays.stream(workflow.getAllowedUsers()).map(ud -> _userHelper.user2json(ud, true)).collect(Collectors.toList()));
1081        jsonData.put("allowedGroups", _groups2Json(workflow.getAllowedGroups()));
1082        if (workflowDefinitionData != null && workflowDefinitionData.containsKey("variables"))
1083        {
1084            @SuppressWarnings("unchecked")
1085            Map<String, Map<String, String>> variables = (Map<String, Map<String, String>>) workflowDefinitionData.get("variables");
1086            if (variables != null)
1087            {
1088                jsonData.put("variables", variables.keySet().stream().filter(key -> workflow.getVariable(key) != null).collect(Collectors.toMap(Function.identity(), key -> workflow.getVariable(key))));
1089            }
1090        }
1091        
1092        jsonData.put("process", getWorkflowProcessus(workflow).size());
1093        
1094        return jsonData;
1095    }
1096
1097    private List<Map<String, Object>> _groups2Json(GroupIdentity[] groups)
1098    {
1099        List<Map<String, Object>> json = new ArrayList<>();
1100        for (GroupIdentity group : groups)
1101        {
1102            Map<String, Object> groupJson = new HashMap<>();
1103            groupJson.put("groupId", group.getId());
1104            groupJson.put("groupDirectory", group.getDirectoryId());
1105            json.add(groupJson);
1106        }
1107        return json;
1108    }
1109
1110    /**
1111     * Gets the root of contents
1112     * @param create <code>true</code> to create automatically the root when missing.
1113     * @return the root of workflows
1114     * @throws UnknownAmetysObjectException If root node does not exist
1115     */
1116    protected ModifiableTraversableAmetysObject _getWorkflowsRootNode(boolean create)
1117    {
1118        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
1119        
1120        boolean needSave = false;
1121        if (!pluginsNode.hasChild(BPM_ROOT_NODE))
1122        {
1123            if (create)
1124            {
1125                pluginsNode.createChild(BPM_ROOT_NODE, "ametys:unstructured");
1126                needSave = true;
1127            }
1128            else
1129            {
1130                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + BPM_ROOT_NODE + "' is missing");
1131            }
1132        }
1133        
1134        ModifiableTraversableAmetysObject bpmNode = pluginsNode.getChild(BPM_ROOT_NODE);
1135        if (!bpmNode.hasChild(BPMWORKFLOW_ROOT_NODE))
1136        {
1137            if (create)
1138            {
1139                bpmNode.createChild(BPMWORKFLOW_ROOT_NODE, "ametys:unstructured");
1140                needSave = true;
1141            }
1142            else
1143            {
1144                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + BPM_ROOT_NODE + "/" + BPMWORKFLOW_ROOT_NODE + "' is missing");
1145            }
1146        }
1147        
1148        if (needSave)
1149        {
1150            pluginsNode.saveChanges();
1151        }
1152        
1153        return bpmNode.getChild(BPMWORKFLOW_ROOT_NODE);
1154    }
1155
1156    /**
1157     * Gets the root of processes of a workflow
1158     * @param workflow The workflow
1159     * @param create <code>true</code> to create automatically the root when missing.
1160     * @return the root of processes
1161     * @throws UnknownAmetysObjectException If root node does not exist
1162     */
1163    protected ModifiableTraversableAmetysObject _getProcessesRootNode(JCRWorkflow workflow, boolean create)
1164    {
1165        boolean needSave = false;
1166        if (!workflow.hasChild(BPMPROCESSES_ROOT_NODE))
1167        {
1168            if (create)
1169            {
1170                workflow.createChild(BPMPROCESSES_ROOT_NODE, "ametys:unstructured");
1171                needSave = true;
1172            }
1173            else
1174            {
1175                throw new UnknownAmetysObjectException("Node '" + BPMPROCESSES_ROOT_NODE + "' is missing for workflow '" + workflow.getId() + "'");
1176            }
1177        }
1178        
1179        if (needSave)
1180        {
1181            workflow.saveChanges();
1182        }
1183        
1184        return workflow.getChild(BPMPROCESSES_ROOT_NODE);
1185    }
1186    
1187
1188    /**
1189     * Get the list of variables and their properties for a workflow definition
1190     * @param workflowDefId The workflow definition id
1191     * @return The list of variables properties, mapped by variable id
1192     */
1193    protected Map<String, Object> _getWorkflowDefinitionData(String workflowDefId)
1194    {
1195        if (_workflowDefinitionsCache.containsKey(workflowDefId))
1196        {
1197            return _workflowDefinitionsCache.get(workflowDefId);
1198        }
1199        
1200        Map<String, Object> workflowDefinitionData = new HashMap<>();
1201        
1202        Map<String, Map<String, Object>> variables = new HashMap<>();
1203        if (!ArrayUtils.contains(_workflowHelper.getWorkflowNames(), workflowDefId))
1204        {
1205            return null;
1206        }
1207        WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowDefId);
1208        if (workflowDescriptor == null)
1209        {
1210            return null;
1211        }
1212        
1213        String workflowXml = workflowDescriptor.asXML();
1214        
1215        try (InputStream is = new ByteArrayInputStream(workflowXml.getBytes()))
1216        {
1217            Configuration workflowConfiguration = new DefaultConfigurationBuilder().build(is);
1218
1219            Configuration[] registers = workflowConfiguration.getChild("registers", true).getChildren("register");
1220            if (registers.length > 0)
1221            {
1222                variables.putAll(_getWorkflowDefinitionRegisters(registers));
1223            }
1224        }
1225        catch (ConfigurationException | IOException | SAXException e)
1226        {
1227            getLogger().warn("Unable to retrieve the workflow definition xml file for '" + workflowDefId + "'", e);
1228            return null;
1229        }
1230        
1231        workflowDefinitionData.put("variables", variables);
1232        _workflowDefinitionsCache.put(workflowDefId, workflowDefinitionData);
1233        return workflowDefinitionData;
1234    }
1235
1236    private Map< ? extends String, ? extends Map<String, Object>> _getWorkflowDefinitionRegisters(Configuration[] registers) throws ConfigurationException
1237    {
1238        Map<String, Map<String, Object>> variables = new HashMap<>();
1239        
1240        for (Configuration register : registers)
1241        {
1242            String variableName = null;
1243            if ("avalon".equals(register.getAttribute("type", null)))
1244            {
1245                Map<String, Object> variableValues = new HashMap<>();
1246                boolean matchRole = false;
1247                for (Configuration arg : register.getChildren("arg"))
1248                {
1249                    String argName = arg.getAttribute("name", null);
1250                    String argValue = arg.getValue();
1251                    if (argName != null)
1252                    {
1253                        if ("role".equals(argName))
1254                        {
1255                            matchRole = RegisterVariable.class.getName().equals(argValue);
1256                        }
1257                        else if ("variableId".equals(argName))
1258                        {
1259                            variableName = argValue;
1260                        }
1261                        else
1262                        {
1263                            if ("label".equals(argName) || "description".equals(argName))
1264                            {
1265                                variableValues.put(argName, new I18nizableText(null, argValue));
1266                            }
1267                            else
1268                            {
1269                                variableValues.put(argName, argValue);
1270                            }
1271                        }
1272                    }
1273                }
1274                
1275                if (matchRole && StringUtils.isNotEmpty(variableName))
1276                {
1277                    variables.put(variableName, variableValues);
1278                }
1279            }
1280        }
1281        
1282        return variables;
1283    }
1284    
1285    private Map<String, Object> _processToJson(JCRWorkflowProcess process)
1286    {
1287        Map<String, Object> jsonData = new HashMap<>();
1288        
1289        jsonData.put("id", process.getId());
1290        jsonData.put("title", process.getTitle());
1291        jsonData.put("creationDate", DateUtils.dateToString(process.getCreationDate()));
1292        jsonData.put("description", process.getDescription());
1293        jsonData.put("creator", _userHelper.user2json(process.getCreator(), true));
1294        
1295        List<Map<String, String>> attachmentsList = new ArrayList<>();
1296        
1297        ResourceCollection rootAttachments = process.getRootAttachments(false);
1298        if (rootAttachments != null)
1299        {
1300            AmetysObjectIterable<ModifiableResource> attachments = rootAttachments.getChildren();
1301            JCRWorkflow workflow = _resolver.resolveById(process.getWorkflow());
1302            
1303            String attachmentPathPrefix = workflow.getName() + "/" + process.getName() + "/_attachments/";
1304            
1305            for (ModifiableResource attachment : attachments)
1306            {
1307                Map<String, String> attachmentData = new HashMap<>();
1308                
1309                String name = attachment.getName();
1310                attachmentData.put("name", name);
1311                attachmentData.put("path", attachmentPathPrefix + name);
1312                
1313                attachmentsList.add(attachmentData);
1314            }
1315        }
1316        
1317        jsonData.put("attachments", attachmentsList);
1318
1319        AmetysObjectWorkflow aoWorkflow = _workflowProvider.getAmetysObjectWorkflow(process, true);
1320        try
1321        {
1322            String workflowName = aoWorkflow.getWorkflowName(process.getWorkflowId());
1323            if (workflowName != null)
1324            {
1325                WorkflowDescriptor workflowDescriptor = aoWorkflow.getWorkflowDescriptor(workflowName);
1326                StepDescriptor step = workflowDescriptor.getStep((int) process.getCurrentStepId());
1327                jsonData.put("currentStep", _i18nUtils.translate(new I18nizableText(null, step.getName())));
1328            }
1329        }
1330        catch (AmetysRepositoryException e)
1331        {
1332            // no workflow node for process
1333        }
1334        
1335        return jsonData;
1336    }
1337
1338
1339}