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())
459                || _rightManager.currentUserHasRight(RIGHT_WORKFLOW_DELETE, null) == RightResult.RIGHT_ALLOW)
460            {
461                List<Object> workflowProcessus = getWorkflowProcessus(workflow);
462                if (workflowProcessus.size() == 0)
463                {
464                    workflows.add(workflow);
465                }
466            }
467        }
468        
469        List<String> workflowDeletedIds = new ArrayList<>();
470        for (JCRWorkflow workflow : workflows)
471        {
472            ModifiableAmetysObject parent = (ModifiableAmetysObject) workflow.getParent();
473            workflowDeletedIds.add(workflow.getId());
474            workflow.remove();
475            parent.saveChanges();
476        }
477        result.put("ids", workflowDeletedIds);
478        
479        result.put("success", true);
480        return result;
481    }
482    
483    /**
484     * Retrieve the list of process created from a workflow
485     * @param workflowId The workflow
486     * @return The list of process
487     */
488    @Callable (right = RIGHTS_PROCESS_MANAGE_PROCESSES)
489    public Map<String, Object> getWorkflowProcesses(String workflowId)
490    {
491        Map<String, Object> result = new HashMap<>();
492        JCRWorkflow workflow = _resolver.resolveById(workflowId);
493        result.put("processes", getWorkflowProcessus(workflow).stream().map(process -> _processToJson((JCRWorkflowProcess) process)).collect(Collectors.toList()));
494        return result;
495    }
496    
497    /**
498     * Retrieve the list of process created from a workflow
499     * @param workflow The workflow
500     * @return The list of process
501     */
502    public List<Object> getWorkflowProcessus(JCRWorkflow workflow)
503    {
504        try
505        {
506            ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, false);
507            return processesRoot.getChildren().stream().collect(Collectors.toList());
508        }
509        catch (UnknownAmetysObjectException e)
510        {
511            // no processes for the workflow
512        }
513        return new ArrayList<>();
514    }
515    
516    
517    /**
518     * Create a new process
519     * @param workflowId The workflow used by the process
520     * @param title The process title
521     * @param site The site on which the process was created
522     * @param description The process description
523     * @param uploadedAttachments The process attachments
524     * @return The new process
525     * @throws WorkflowException If an error occurred creating the workflow
526     * @throws IllegalAccessException If the user is not allowed
527     */
528    public JCRWorkflowProcess createProcess(String workflowId, String title, String site, String description, List<PartOnDisk> uploadedAttachments) throws WorkflowException, IllegalAccessException
529    {
530        JCRWorkflow workflow = _resolver.resolveById(workflowId);
531        
532        if (!isUserAllowedOnWorkflow(workflow))
533        {
534            throw new IllegalAccessException("User '" + _currentUserProvider.getUser().toString() + "' tried to create a process with insufficient rights");
535        }
536        
537        ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, true);
538        
539        String originalName = FilterNameHelper.filterName(title);
540        // Find unique name
541        String uniqueName = originalName;
542        int index = 2;
543        while (processesRoot.hasChild(uniqueName))
544        {
545            uniqueName = originalName + "-" + (index++);
546        }
547
548        JCRWorkflowProcess process = (JCRWorkflowProcess) processesRoot.createChild(uniqueName, JCRWorkflowProcessFactory.WORKFLOW_PROCESS_NODE_TYPE);
549        process.setCreator(_currentUserProvider.getUser());
550        process.setWorkflow(workflowId);
551        process.setCreationDate(new Date());
552        _setProcessValues(process, title, site, description, uploadedAttachments);
553        
554        // create workflow with entry id
555        Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
556        
557        String workflowName = workflow.getWorkflowDefinition();
558        int initialActionId = _workflowHelper.getInitialAction(workflowName); 
559        
560        // Add the workflow used by the process in the request, to be used by any RegisterVariable of the workflow definition 
561        Request request = ContextHelper.getRequest(_context);
562        request.setAttribute("workflowId", process.getWorkflow());
563        
564        Map<String, Object> inputs = new HashMap<>();
565        inputs.put("workflowId", workflowId);
566        inputs.put("process", process);
567        long workflowInstanceId = workflowStore.initialize(workflowName, initialActionId, inputs);
568        process.setWorkflowId(workflowInstanceId);
569        Step currentStep = (Step) workflowStore.getCurrentSteps(process.getWorkflowId()).iterator().next();
570        process.setCurrentStepId(currentStep.getStepId());
571        process.saveChanges();
572        
573        return process;
574    }
575    
576
577    /**
578     * Edit a process
579     * @param processId The process id
580     * @param title The title
581     * @param description The description
582     * @param uploadedAttachments The list of new attachments uploaded
583     * @param attachmentsUntouched The list of untouched attachments names
584     * @throws WorkflowException If an error occurred while executing the workflow action 
585     */
586    public void editProcess(String processId, String title, String description, List<PartOnDisk> uploadedAttachments, List<String> attachmentsUntouched) throws WorkflowException
587    {
588        JCRWorkflowProcess process = _resolver.resolveById(processId);
589        
590        Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
591        Map<String, Object> inputs = new HashMap<>();
592        inputs.put("process", process);
593        
594        inputs.put("title", title);
595        inputs.put("description", description);
596        inputs.put("uploadedAttachments", uploadedAttachments);
597        inputs.put("attachmentsUntouched", attachmentsUntouched);
598        
599        // Add the workflow used by the process in the request, to be used by any RegisterVariable of the workflow definition 
600        Request request = ContextHelper.getRequest(_context);
601        request.setAttribute("workflowId", process.getWorkflow());
602        
603        workflowStore.doAction(process.getWorkflowId(), WORKFLOW_ACTION_EDIT, inputs);
604    }
605    
606    /**
607     * Retrieve a process
608     * @param workflowName The workflow name
609     * @param processName The process name
610     * @return The process
611     */
612    public JCRWorkflowProcess getProcess(String workflowName, String processName)
613    {
614        try
615        {
616            ModifiableTraversableAmetysObject workflowsRoot = _getWorkflowsRootNode(false);
617            JCRWorkflow workflow = workflowsRoot.getChild(workflowName);
618            ModifiableTraversableAmetysObject processesRoot = _getProcessesRootNode(workflow, false);
619            if (processesRoot.hasChild(processName))
620            {
621                return processesRoot.getChild(processName);
622            }
623        }
624        catch (UnknownAmetysObjectException e)
625        {
626            // No workflow or process created with this workflowId and processId
627            return null;
628        }
629        
630        return null;
631    }
632    
633    /**
634     * Determines if the process is complete
635     * @param process The process
636     * @return True if the process is complete
637     */
638    public boolean isComplete(JCRWorkflowProcess process)
639    {
640        AmetysObjectWorkflow aoWorkflow = _workflowProvider.getAmetysObjectWorkflow(process, true);
641        if (aoWorkflow != null)
642        {
643            int entryState = aoWorkflow.getEntryState(process.getWorkflowId());
644            return entryState ==  WorkflowEntry.COMPLETED;
645        }
646        
647        // FIXME on final step the workflow is compelte but deleted ...
648        return false;
649    }
650    
651    /**
652     * Determines if the process can be deleted by the current user
653     * @param process The process
654     * @return True if the process can be deleted
655     */
656    public boolean canDelete(JCRWorkflowProcess process)
657    {
658        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) == RightResult.RIGHT_ALLOW)
659        {
660            // A super admin can delete a process at all time
661            return true;
662        }
663        
664        // Otherwise a process can be deleted only by the creator if is complete
665        return isComplete(process) && process.getCreator().equals(_currentUserProvider.getUser());
666    }
667
668    /**
669     * Delete a process
670     * @param process The process to delete
671     * @throws IllegalAccessException If a user tries to delete a process without being allowed 
672     */
673    public void deleteProcess(JCRWorkflowProcess process) throws IllegalAccessException
674    {
675        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) != RightResult.RIGHT_ALLOW)
676        {
677            Workflow workflowStore = _workflowProvider.getAmetysObjectWorkflow(process, true);
678            int entryState = workflowStore.getEntryState(process.getWorkflowId());
679            if (entryState !=  WorkflowEntry.COMPLETED || !process.getCreator().equals(_currentUserProvider.getUser()))
680            {
681                throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete a process without sufficient rights."); 
682            }
683        }
684        
685        ModifiableAmetysObject parent = process.getParent();
686        process.remove();
687        parent.saveChanges();
688    }
689
690    /**
691     * Delete a list of processes
692     * @param processes The processes to delete
693     * @return The result
694     * @throws IllegalAccessException If a user tries to delete a process without being allowed 
695     */
696    @Callable (right = RIGHTS_PROCESS_MANAGE_PROCESSES)
697    public Map<String, Object> deleteProcesses(List<String> processes) throws IllegalAccessException
698    {
699        Map<String, Object> result = new HashMap<>();
700        List<String> processesDeletedIds = new ArrayList<>();
701        for (String processId : processes)
702        {
703            JCRWorkflowProcess process = _resolver.resolveById(processId);
704            ModifiableAmetysObject parent = (ModifiableAmetysObject) process.getParent();
705            processesDeletedIds.add(process.getId());
706            process.remove();
707            parent.saveChanges();
708        }
709
710        result.put("ids", processesDeletedIds);
711        result.put("success", true);
712        return result;
713    }
714    
715    /**
716     * Get the url of a process page
717     * @param process The process
718     * @param siteName The current site name. Can not be null.
719     * @param lang The current language. Can not be null.
720     * @param absolute true to get absolute url.
721     * @return the url of process page
722     */
723    public String getProcessPageUrl(JCRWorkflowProcess process, String siteName, String lang, boolean absolute)
724    {
725        StringBuilder sb = new StringBuilder();
726        
727        if (absolute)
728        {
729            sb.append(_uriPrefixHandler.getAbsoluteUriPrefix(siteName));
730        }
731        else
732        {
733            sb.append(_uriPrefixHandler.getUriPrefix(siteName));
734        }
735        
736        sb.append("/")
737            .append(lang)
738            .append("/_plugins/")
739            .append(_pluginName)
740            .append("/")
741            .append(getTemplateForProcessPage(siteName, lang))
742            .append("/process/");
743        
744        JCRWorkflow workflow = _resolver.resolveById(process.getWorkflow());
745        
746        sb.append(URIUtils.encodePathSegment(workflow.getName()))
747            .append("/")
748            .append(URIUtils.encodePathSegment(process.getName()))
749            .append(".html");
750        
751        return sb.toString();
752    }
753    
754    /**
755     * Get the page with PROCESS_DASHBOARD tag
756     * @param siteName The current site name
757     * @param lang The language
758     * @return The page or null if not found.
759     */
760    public Page getDashboardPage (String siteName, String lang)
761    {
762        TagExpression tagExpression = new TagExpression(Operator.EQ, PAGE_TAG_PROCESS_DASHBOARD);
763        
764        String pathQuery = PageQueryHelper.getPageXPathQuery(siteName, lang, null, tagExpression, null);
765        AmetysObjectIterable<Page> page = _ametysObjectResolver.query(pathQuery);
766        
767        if (page.getSize() == 0)
768        {
769            return null;
770        }
771        
772        return page.iterator().next();
773    }
774    
775    /**
776     * Get the page with CREATE_PROCESS tag
777     * @param siteName The current site name
778     * @param lang The language
779     * @return The page or null if not found.
780     */
781    public Page getCreateProcessPage (String siteName, String lang)
782    {
783        TagExpression tagExpression = new TagExpression(Operator.EQ, PAGE_TAG_CREATEPROCESS);
784        
785        String pathQuery = PageQueryHelper.getPageXPathQuery(siteName, lang, null, tagExpression, null);
786        AmetysObjectIterable<Page> page = _ametysObjectResolver.query(pathQuery);
787        
788        if (page.getSize() == 0)
789        {
790            return null;
791        }
792        
793        return page.iterator().next();
794    }
795    
796    /**
797     * Get the template to use for process rendering
798     * @param siteName The site containing the page
799     * @param lang The sitemap of the page
800     * @return The template to use for process rendering
801     */
802    public String getTemplateForProcessPage(String siteName, String lang)
803    {
804        Site site = _siteManager.getSite(siteName);
805        
806        String skinName = site.getSkinId();
807        Skin skin = _skinsManager.getSkin(skinName);
808        Set<String> templates = skin.getTemplates();
809        
810        // First look for the template bpm-process
811        if (templates.contains(_BPM_PROCESS_TEMPLATE))
812        {
813            return _BPM_PROCESS_TEMPLATE;
814        }
815
816        // Otherwise, returns the template used by create process page
817        Page createProcessPage = getCreateProcessPage(siteName, lang);
818        if (createProcessPage != null)
819        {
820            return createProcessPage.getTemplate();
821        }
822        
823        // Otherwise, fallback to the template page if exists
824        if (templates.contains("page"))
825        {
826            return "page";
827        }
828        
829        // Finally, fallback to the first template
830        return templates.iterator().next();
831    }
832    
833    /**
834     * Test if the current user is present in any variable of the workflow
835     * @param workflow The workflow
836     * @return True if the user is in the workflow variables
837     */
838    @SuppressWarnings("unchecked")
839    public boolean isUserInWorkflowVariables(JCRWorkflow workflow)
840    {
841        Map<String, Object> workflowDefinitionData = _getWorkflowDefinitionData(workflow.getWorkflowDefinition());
842        
843        if (workflowDefinitionData != null && workflowDefinitionData.containsKey("variables"))
844        {
845            Map<String, Map<String, String>> variables = (Map<String, Map<String, String>>) workflowDefinitionData.get("variables");
846            UserIdentity currentUser = _currentUserProvider.getUser();
847            
848            boolean userInVariables = variables.entrySet().stream()
849                .filter(e -> "user".equals(e.getValue().getOrDefault("type", null)))
850                .map(e -> workflow.getVariable(e.getKey()))
851                .filter(var -> var != null && var instanceof String)
852                .map(var -> _jsonUtils.convertJsonToList((String) var))
853                .map(usersList ->
854                {
855                    return usersList.stream()
856                        .map(userData ->
857                        {
858                            Map<String, String> user = (Map<String, String>) userData;
859                            String login = user.get("login");
860                            String populationId = user.get("populationId");
861                            return new UserIdentity(login, populationId);
862                        })
863                        .filter(user -> user != null && user.equals(currentUser))
864                        .findAny();
865                })
866                .filter(optionalUser -> optionalUser.isPresent())
867                .findAny()
868                .isPresent();
869
870            return userInVariables;
871        }
872        return false;
873    }
874
875    /**
876     * Get the list of processes accessibles to the current user
877     * @return The list of processes
878     */
879    public Set<JCRWorkflowProcess> getUserProcesses()
880    {
881        Set<JCRWorkflowProcess> processes = new HashSet<>();
882        UserIdentity user = _currentUserProvider.getUser();
883        if (user == null)
884        {
885            return processes;
886        }
887        
888        if (_rightManager.currentUserHasRight(RIGHTS_PROCESS_MANAGE_PROCESSES, null) == RightResult.RIGHT_ALLOW)
889        {
890            // return all processes as we have the right to manage all of them
891            AmetysObjectIterable<JCRWorkflowProcess> processesOwned = _ametysObjectResolver.query("//element(*, ametys:workflowProcess)");
892            Iterator<JCRWorkflowProcess> it = processesOwned.iterator();
893            while (it.hasNext())
894            {
895                processes.add(it.next());
896            }
897            return processes;
898        }
899        
900        // retrieve processes of which we are the creator
901        String xpath = String.format("//element(*, ametys:workflowProcess)[@ametys:creator/ametys:login='%s' and @ametys:creator/ametys:population='%s']", user.getLogin(), user.getPopulationId());
902        AmetysObjectIterable<JCRWorkflowProcess> processesOwned = _ametysObjectResolver.query(xpath);
903        Iterator<JCRWorkflowProcess> it = processesOwned.iterator();
904        while (it.hasNext())
905        {
906            processes.add(it.next());
907        }
908        
909        // add processes for which we are in the workflow variables
910        ModifiableTraversableAmetysObject workflowsRootNode = _getWorkflowsRootNode(false);
911        AmetysObjectIterable<AmetysObject> children = workflowsRootNode.getChildren();
912        for (AmetysObject child : children)
913        {
914            if (child instanceof JCRWorkflow)
915            {
916                JCRWorkflow workflow = (JCRWorkflow) child;
917                try
918                {
919                    ModifiableTraversableAmetysObject processesRootNode = _getProcessesRootNode(workflow, false);
920                    AmetysObjectIterable<JCRWorkflowProcess> workflowChildren = processesRootNode.getChildren();
921                    if (workflowChildren.getSize() > 0 && isUserInWorkflowVariables(workflow))
922                    {
923                        AmetysObjectIterator<JCRWorkflowProcess> it2 = workflowChildren.iterator();
924                        while (it2.hasNext())
925                        {
926                            processes.add(it2.next());
927                        }
928                    }
929                }
930                catch (UnknownAmetysObjectException e)
931                {
932                    // No process for workflow, do nothing
933                }
934            }
935        }
936        
937        return processes;
938    }
939
940    /**
941     * Check if the current user is allowed to create a process from the workflow 
942     * @param workflow The workflow
943     * @return True if the user is allowed
944     */
945    public boolean isUserAllowedOnWorkflow(JCRWorkflow workflow)
946    {
947        UserIdentity user = _currentUserProvider.getUser();
948        if (user != null)
949        {
950            if (user.equals(workflow.getOwner()) || ArrayUtils.contains(workflow.getAllowedUsers(), user))
951            {
952                return true;
953            }
954            
955            GroupIdentity[] allowedGroups = workflow.getAllowedGroups();
956            for (GroupIdentity userGroup : _groupManager.getUserGroups(user))
957            {
958                if (ArrayUtils.contains(allowedGroups, userGroup))
959                {
960                    return true;
961                }
962            }
963        
964        }
965        return false;
966    }
967
968    @SuppressWarnings("unchecked")
969    private void _setWorkflowValues(JCRWorkflow workflow, Map<String, Object> values)
970    {
971        String title = (String) values.get("title");
972        String description = (String) values.getOrDefault("description", null);
973        Map<String, Object> owner = (Map<String, Object>) values.getOrDefault("owner", null);
974        List<Map<String, Object>> allowedUsersValue = (List<Map<String, Object>>) values.getOrDefault("allowedUsers", null);
975        List<Map<String, Object>> allowedGroupsValue = (List<Map<String, Object>>) values.getOrDefault("allowedGroups", null);
976        Map<String, Object> variables = (Map<String, Object>) values.getOrDefault("variables", null);
977
978        workflow.setTitle(title);
979        workflow.setDescription(description);
980        workflow.setOwner(owner != null ? _convertJsonToUser(owner) : null);
981
982        if (allowedUsersValue != null)
983        {
984            Set<UserIdentity> allowedUsers = new HashSet<>();
985            for (Object obj : allowedUsersValue)
986            {
987                UserIdentity user = _convertJsonToUser((Map<String, Object>) obj);
988                if (user != null)
989                {
990                    allowedUsers.add(user);
991                }
992            }
993            
994            workflow.setAllowedUsers(allowedUsers.toArray(new UserIdentity[allowedUsers.size()]));
995        }
996        
997        if (allowedGroupsValue != null)
998        {
999            Set<GroupIdentity> allowedGroups = new HashSet<>();
1000            for (Object obj : allowedGroupsValue)
1001            {
1002                Map<String, Object> grantedUser = (Map<String, Object>) obj;
1003                String groupId = (String) grantedUser.get("groupId");
1004                String groupDirectory = (String) grantedUser.get("groupDirectory");
1005                
1006                if (groupId != null && groupDirectory != null)
1007                {
1008                    allowedGroups.add(new GroupIdentity(groupId, groupDirectory));
1009                }
1010            }
1011
1012            workflow.setAllowedGroups(allowedGroups.toArray(new GroupIdentity[allowedGroups.size()]));
1013        }
1014        
1015        for (Entry<String, Object> entry : variables.entrySet())
1016        {
1017            workflow.setVariable(entry.getKey(), entry.getValue());
1018        }
1019    }
1020    
1021    private void _setProcessValues(JCRWorkflowProcess process, String title, String site, String description, List<PartOnDisk> uploadedAttachments)
1022    {
1023        if (title != null)
1024        {
1025            process.setTitle(title);
1026        }
1027        
1028        if (site != null)
1029        {
1030            process.setSite(site);
1031        }
1032        
1033        process.setDescription(description);
1034        
1035        ResourceCollection rootAttachments = process.getRootAttachments(true);
1036        if (rootAttachments instanceof ModifiableResourceCollection)
1037        {
1038            _addOrUpdateResourceHelper.checkAddResourceRight((ModifiableResourceCollection) rootAttachments);
1039            
1040            for (Part attachment : uploadedAttachments)
1041            {
1042                _addOrUpdateResourceHelper.performResourceOperation(attachment, (ModifiableResourceCollection) rootAttachments, ResourceOperationMode.ADD);
1043            }
1044        }
1045    }
1046
1047    private UserIdentity _convertJsonToUser(Map<String, Object> user)
1048    {
1049        String login = (String) user.get("login");
1050        String populationId = (String) user.get("populationId");
1051        
1052        if (login != null && populationId != null)
1053        {
1054            return new UserIdentity(login, populationId);
1055        }
1056        
1057        return null;
1058    }
1059    
1060    private Map<String, Object> _workflowToJson(JCRWorkflow workflow, Map<String, Object> workflowDefinitionData)
1061    {
1062        if (workflow == null)
1063        {
1064            return null;
1065        }
1066        
1067        Map<String, Object> jsonData = new HashMap<>();
1068        
1069        jsonData.put("id", workflow.getId());
1070        jsonData.put("title", workflow.getTitle());
1071        Map<String, Object> workflowBuffer = new HashMap<>();
1072        workflowBuffer.put("value", workflow.getWorkflowDefinition());
1073        workflowBuffer.put("label", new I18nizableText("application", "WORKFLOW_" + workflow.getWorkflowDefinition()));
1074        if (workflowDefinitionData == null)
1075        {
1076            workflowBuffer.put("missing", true);
1077        }
1078        jsonData.put("workflowDefinition", workflowBuffer);
1079        jsonData.put("description", workflow.getDescription());
1080        jsonData.put("owner", _userHelper.user2json(workflow.getOwner(), true));
1081        jsonData.put("allowedUsers", Arrays.stream(workflow.getAllowedUsers()).map(ud -> _userHelper.user2json(ud, true)).collect(Collectors.toList()));
1082        jsonData.put("allowedGroups", _groups2Json(workflow.getAllowedGroups()));
1083        if (workflowDefinitionData != null && workflowDefinitionData.containsKey("variables"))
1084        {
1085            @SuppressWarnings("unchecked")
1086            Map<String, Map<String, String>> variables = (Map<String, Map<String, String>>) workflowDefinitionData.get("variables");
1087            if (variables != null)
1088            {
1089                jsonData.put("variables", variables.keySet().stream().filter(key -> workflow.getVariable(key) != null).collect(Collectors.toMap(Function.identity(), key -> workflow.getVariable(key))));
1090            }
1091        }
1092        
1093        jsonData.put("process", getWorkflowProcessus(workflow).size());
1094        
1095        return jsonData;
1096    }
1097
1098    private List<Map<String, Object>> _groups2Json(GroupIdentity[] groups)
1099    {
1100        List<Map<String, Object>> json = new ArrayList<>();
1101        for (GroupIdentity group : groups)
1102        {
1103            Map<String, Object> groupJson = new HashMap<>();
1104            groupJson.put("groupId", group.getId());
1105            groupJson.put("groupDirectory", group.getDirectoryId());
1106            json.add(groupJson);
1107        }
1108        return json;
1109    }
1110
1111    /**
1112     * Gets the root of contents
1113     * @param create <code>true</code> to create automatically the root when missing.
1114     * @return the root of workflows
1115     * @throws UnknownAmetysObjectException If root node does not exist
1116     */
1117    protected ModifiableTraversableAmetysObject _getWorkflowsRootNode(boolean create)
1118    {
1119        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
1120        
1121        boolean needSave = false;
1122        if (!pluginsNode.hasChild(BPM_ROOT_NODE))
1123        {
1124            if (create)
1125            {
1126                pluginsNode.createChild(BPM_ROOT_NODE, "ametys:unstructured");
1127                needSave = true;
1128            }
1129            else
1130            {
1131                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + BPM_ROOT_NODE + "' is missing");
1132            }
1133        }
1134        
1135        ModifiableTraversableAmetysObject bpmNode = pluginsNode.getChild(BPM_ROOT_NODE);
1136        if (!bpmNode.hasChild(BPMWORKFLOW_ROOT_NODE))
1137        {
1138            if (create)
1139            {
1140                bpmNode.createChild(BPMWORKFLOW_ROOT_NODE, "ametys:unstructured");
1141                needSave = true;
1142            }
1143            else
1144            {
1145                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + BPM_ROOT_NODE + "/" + BPMWORKFLOW_ROOT_NODE + "' is missing");
1146            }
1147        }
1148        
1149        if (needSave)
1150        {
1151            pluginsNode.saveChanges();
1152        }
1153        
1154        return bpmNode.getChild(BPMWORKFLOW_ROOT_NODE);
1155    }
1156
1157    /**
1158     * Gets the root of processes of a workflow
1159     * @param workflow The workflow
1160     * @param create <code>true</code> to create automatically the root when missing.
1161     * @return the root of processes
1162     * @throws UnknownAmetysObjectException If root node does not exist
1163     */
1164    protected ModifiableTraversableAmetysObject _getProcessesRootNode(JCRWorkflow workflow, boolean create)
1165    {
1166        boolean needSave = false;
1167        if (!workflow.hasChild(BPMPROCESSES_ROOT_NODE))
1168        {
1169            if (create)
1170            {
1171                workflow.createChild(BPMPROCESSES_ROOT_NODE, "ametys:unstructured");
1172                needSave = true;
1173            }
1174            else
1175            {
1176                throw new UnknownAmetysObjectException("Node '" + BPMPROCESSES_ROOT_NODE + "' is missing for workflow '" + workflow.getId() + "'");
1177            }
1178        }
1179        
1180        if (needSave)
1181        {
1182            workflow.saveChanges();
1183        }
1184        
1185        return workflow.getChild(BPMPROCESSES_ROOT_NODE);
1186    }
1187    
1188
1189    /**
1190     * Get the list of variables and their properties for a workflow definition
1191     * @param workflowDefId The workflow definition id
1192     * @return The list of variables properties, mapped by variable id
1193     */
1194    protected Map<String, Object> _getWorkflowDefinitionData(String workflowDefId)
1195    {
1196        if (_workflowDefinitionsCache.containsKey(workflowDefId))
1197        {
1198            return _workflowDefinitionsCache.get(workflowDefId);
1199        }
1200        
1201        Map<String, Object> workflowDefinitionData = new HashMap<>();
1202        
1203        Map<String, Map<String, Object>> variables = new HashMap<>();
1204        Workflow externalWorkflow = _workflowProvider.getExternalWorkflow(JdbcWorkflowStore.ROLE);
1205        if (!ArrayUtils.contains(externalWorkflow.getWorkflowNames(), workflowDefId))
1206        {
1207            return null;
1208        }
1209        WorkflowDescriptor workflowDescriptor = externalWorkflow.getWorkflowDescriptor(workflowDefId);
1210        if (workflowDescriptor == null)
1211        {
1212            return null;
1213        }
1214        
1215        String workflowXml = workflowDescriptor.asXML();
1216        
1217        try (InputStream is = new ByteArrayInputStream(workflowXml.getBytes()))
1218        {
1219            Configuration workflowConfiguration = new DefaultConfigurationBuilder().build(is);
1220
1221            Configuration[] registers = workflowConfiguration.getChild("registers", true).getChildren("register");
1222            if (registers.length > 0)
1223            {
1224                variables.putAll(_getWorkflowDefinitionRegisters(registers));
1225            }
1226        }
1227        catch (ConfigurationException | IOException | SAXException e)
1228        {
1229            getLogger().warn("Unable to retrieve the workflow definition xml file for '" + workflowDefId + "'", e);
1230            return null;
1231        }
1232        
1233        workflowDefinitionData.put("variables", variables);
1234        _workflowDefinitionsCache.put(workflowDefId, workflowDefinitionData);
1235        return workflowDefinitionData;
1236    }
1237
1238    private Map< ? extends String, ? extends Map<String, Object>> _getWorkflowDefinitionRegisters(Configuration[] registers) throws ConfigurationException
1239    {
1240        Map<String, Map<String, Object>> variables = new HashMap<>();
1241        
1242        for (Configuration register : registers)
1243        {
1244            String variableName = null;
1245            if ("avalon".equals(register.getAttribute("type", null)))
1246            {
1247                Map<String, Object> variableValues = new HashMap<>();
1248                boolean matchRole = false;
1249                for (Configuration arg : register.getChildren("arg"))
1250                {
1251                    String argName = arg.getAttribute("name", null);
1252                    String argValue = arg.getValue();
1253                    if (argName != null)
1254                    {
1255                        if ("role".equals(argName))
1256                        {
1257                            matchRole = RegisterVariable.class.getName().equals(argValue);
1258                        }
1259                        else if ("variableId".equals(argName))
1260                        {
1261                            variableName = argValue;
1262                        }
1263                        else
1264                        {
1265                            if ("label".equals(argName) || "description".equals(argName))
1266                            {
1267                                variableValues.put(argName, new I18nizableText(null, argValue));
1268                            }
1269                            else
1270                            {
1271                                variableValues.put(argName, argValue);
1272                            }
1273                        }
1274                    }
1275                }
1276                
1277                if (matchRole && StringUtils.isNotEmpty(variableName))
1278                {
1279                    variables.put(variableName, variableValues);
1280                }
1281            }
1282        }
1283        
1284        return variables;
1285    }
1286    
1287    private Map<String, Object> _processToJson(JCRWorkflowProcess process)
1288    {
1289        Map<String, Object> jsonData = new HashMap<>();
1290        
1291        jsonData.put("id", process.getId());
1292        jsonData.put("title", process.getTitle());
1293        jsonData.put("creationDate", DateUtils.dateToString(process.getCreationDate()));
1294        jsonData.put("description", process.getDescription());
1295        jsonData.put("creator", _userHelper.user2json(process.getCreator(), true));
1296        
1297        List<Map<String, String>> attachmentsList = new ArrayList<>();
1298        
1299        ResourceCollection rootAttachments = process.getRootAttachments(false);
1300        if (rootAttachments != null)
1301        {
1302            AmetysObjectIterable<ModifiableResource> attachments = rootAttachments.getChildren();
1303            JCRWorkflow workflow = _resolver.resolveById(process.getWorkflow());
1304            
1305            String attachmentPathPrefix = workflow.getName() + "/" + process.getName() + "/_attachments/";
1306            
1307            for (ModifiableResource attachment : attachments)
1308            {
1309                Map<String, String> attachmentData = new HashMap<>();
1310                
1311                String name = attachment.getName();
1312                attachmentData.put("name", name);
1313                attachmentData.put("path", attachmentPathPrefix + name);
1314                
1315                attachmentsList.add(attachmentData);
1316            }
1317        }
1318        
1319        jsonData.put("attachments", attachmentsList);
1320
1321        AmetysObjectWorkflow aoWorkflow = _workflowProvider.getAmetysObjectWorkflow(process, true);
1322        try
1323        {
1324            String workflowName = aoWorkflow.getWorkflowName(process.getWorkflowId());
1325            if (workflowName != null)
1326            {
1327                WorkflowDescriptor workflowDescriptor = aoWorkflow.getWorkflowDescriptor(workflowName);
1328                StepDescriptor step = workflowDescriptor.getStep((int) process.getCurrentStepId());
1329                jsonData.put("currentStep", _i18nUtils.translate(new I18nizableText(null, step.getName())));
1330            }
1331        }
1332        catch (AmetysRepositoryException e)
1333        {
1334            // no workflow node for process
1335        }
1336        
1337        return jsonData;
1338    }
1339
1340
1341}