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