001/*
002 *  Copyright 2015 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.explorer.calendars.actions;
017
018import java.time.ZonedDateTime;
019import java.time.format.DateTimeFormatter;
020import java.util.ArrayList;
021import java.util.Date;
022import java.util.GregorianCalendar;
023import java.util.HashMap;
024import java.util.LinkedList;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.jcr.RepositoryException;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.logger.AbstractLogEnabled;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.lang.BooleanUtils;
037import org.apache.commons.lang.IllegalClassException;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.jackrabbit.util.Text;
040
041import org.ametys.core.observation.Event;
042import org.ametys.core.observation.ObservationManager;
043import org.ametys.core.right.RightManager;
044import org.ametys.core.right.RightManager.RightResult;
045import org.ametys.core.ui.Callable;
046import org.ametys.core.user.CurrentUserProvider;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.user.UserManager;
050import org.ametys.plugins.explorer.ExplorerNode;
051import org.ametys.plugins.explorer.ModifiableExplorerNode;
052import org.ametys.plugins.explorer.ObservationConstants;
053import org.ametys.plugins.explorer.calendars.Calendar;
054import org.ametys.plugins.explorer.calendars.Calendar.CalendarVisibility;
055import org.ametys.plugins.explorer.calendars.CalendarEvent;
056import org.ametys.plugins.explorer.calendars.ModifiableCalendar;
057import org.ametys.plugins.explorer.calendars.ModifiableCalendarEvent;
058import org.ametys.plugins.explorer.calendars.jcr.JCRCalendar;
059import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEvent;
060import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarFactory;
061import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
062import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO;
063import org.ametys.plugins.explorer.workflow.AbstractExplorerNodeWorkflowComponent;
064import org.ametys.plugins.repository.AmetysObject;
065import org.ametys.plugins.repository.AmetysObjectIterable;
066import org.ametys.plugins.repository.AmetysObjectResolver;
067import org.ametys.plugins.repository.AmetysRepositoryException;
068import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
069import org.ametys.plugins.workflow.support.WorkflowHelper;
070import org.ametys.plugins.workflow.support.WorkflowProvider;
071import org.ametys.runtime.i18n.I18nizableText;
072import org.ametys.runtime.parameter.ParameterHelper;
073import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
074
075import com.opensymphony.workflow.Workflow;
076import com.opensymphony.workflow.WorkflowException;
077import com.opensymphony.workflow.spi.Step;
078
079/**
080 * Calendar DAO
081 */
082public class CalendarDAO extends AbstractLogEnabled implements Serviceable, Component
083{
084    /** Avalon Role */
085    public static final String ROLE = CalendarDAO.class.getName();
086    
087    /** Right to add a calendar */
088    public static final String RIGHTS_CALENDAR_ADD = "Plugin_Explorer_Calendar_Add";
089    /** Right to edit a calendar */
090    public static final String RIGHTS_CALENDAR_EDIT = "Plugin_Explorer_Calendar_Edit";
091    /** Right to delete a calendar */
092    public static final String RIGHTS_CALENDAR_DELETE = "Plugin_Explorer_Calendar_Delete";
093    /** Right to add a event */
094    public static final String RIGHTS_EVENT_ADD = "Plugin_Explorer_Event_Add";
095    /** Right to edit a event */
096    public static final String RIGHTS_EVENT_EDIT = "Plugin_Explorer_Event_Edit";
097    /** Right to propose a event */
098    public static final String RIGHTS_EVENT_PROPOSE = "Plugin_Explorer_Event_Propose";
099    /** Right to validate a event */
100    public static final String RIGHTS_EVENT_VALIDATE = "Plugin_Explorer_Event_Validate";
101    /** Right to refuse a event */
102    public static final String RIGHTS_EVENT_REFUSE = "Plugin_Explorer_Event_Refuse";
103    /** Right to delete a event */
104    public static final String RIGHTS_EVENT_DELETE = "Plugin_Explorer_Event_Delete";
105    /** Right to delete_own a event */
106    public static final String RIGHTS_EVENT_DELETE_OWN = "Plugin_Explorer_Owned_Event_Delete";
107    
108    /** Explorer resources DAO */
109    protected ExplorerResourcesDAO _explorerResourcesDAO;
110    
111    /** Ametys resolver */
112    protected AmetysObjectResolver _resolver;
113    
114    /** Observer manager. */
115    protected ObservationManager _observationManager;
116    
117    /** The current user provider. */
118    protected CurrentUserProvider _currentUserProvider;
119    
120    /** The rights manager */
121    protected RightManager _rightManager;
122    
123    /** User manager */
124    protected UserManager _userManager;
125    
126    /** The workflow provider */
127    protected WorkflowProvider _workflowProvider;
128    
129    /** The workflow helper */
130    protected WorkflowHelper _workflowHelper;
131
132    
133    public void service(ServiceManager manager) throws ServiceException
134    {
135        _explorerResourcesDAO = (ExplorerResourcesDAO) manager.lookup(ExplorerResourcesDAO.ROLE);
136        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
137        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
138        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
139        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
140        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
141        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
142        _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE);
143    }
144    
145    /**
146     * Get calendar info
147     * @param id The calendar id
148     * @param recursive True to get data for sub calendars
149     * @param includeEvents True to also include child events
150     * @return the calendar data in a map
151     */
152    @Callable
153    public Map<String, Object> getCalendarData(String id, boolean recursive, boolean includeEvents)
154    {
155        Calendar calendar = (Calendar) _resolver.resolveById(id);
156        return getCalendarData(calendar, recursive, includeEvents);
157    }
158    
159    /**
160     * Get calendar info
161     * @param calendar The calendar
162     * @param recursive True to get data for sub calendars
163     * @param includeEvents True to also include child events
164     * @return the calendar data in a map
165     */
166    public Map<String, Object> getCalendarData(Calendar calendar, boolean recursive, boolean includeEvents)
167    {
168        Map<String, Object> result = new HashMap<>();
169        
170        result.put("id", calendar.getId());
171        result.put("title", Text.unescapeIllegalJcrChars(calendar.getName()));
172        result.put("description", calendar.getDescription());
173        result.put("templateDesc", calendar.getTemplateDescription());
174        result.put("color", calendar.getColor());
175        result.put("visibility", calendar.getVisibility().name().toLowerCase());
176        result.put("workflowName", calendar.getWorkflowName());
177        
178        if (recursive)
179        {
180            List<Map<String, Object>> calendarList = new LinkedList<>();
181            result.put("calendars", calendarList);
182            
183            AmetysObjectIterable<AmetysObject> children = calendar.getChildren();
184            for (AmetysObject child : children)
185            {
186                if (child instanceof Calendar)
187                {
188                    calendarList.add(getCalendarData((Calendar) child, recursive, includeEvents));
189                }
190            }
191        }
192        
193        if (includeEvents)
194        {
195            List<Map<String, Object>> eventList = new LinkedList<>();
196            result.put("events", eventList);
197            
198            AmetysObjectIterable<AmetysObject> children = calendar.getChildren();
199            for (AmetysObject child : children)
200            {
201                if (child instanceof CalendarEvent)
202                {
203                    eventList.add(getEventData((CalendarEvent) child, false));
204                }
205            }
206        }
207        
208        return result;
209    }
210    
211    /**
212     * Get the template description of a calendar
213     * @param calendarId The identifier of the calendar
214     * @return The template description
215     */
216    @Callable
217    public String getTemplateDescription(String calendarId)
218    {
219        Calendar calendar = _resolver.resolveById(calendarId);
220        return StringUtils.defaultString(calendar.getTemplateDescription());
221    }
222    
223    /**
224     * Get event info
225     * @param ids The event ids
226     * @param fullInfo true to include full info (rights, parent id, etc...)
227     * @return the list of event data
228     */
229    @Callable
230    public List<Map<String, Object>> getEventsDataByIds(List<String> ids, boolean fullInfo)
231    {
232        List<CalendarEvent> events = new LinkedList<>();
233        for (String id : ids)
234        {
235            events.add((CalendarEvent) _resolver.resolveById(id));
236        }
237        
238        return getEventsData(events, fullInfo);
239    }
240    
241    /**
242     * Get event info
243     * @param events The events
244     * @param fullInfo true to include full info (rights, parent id, etc...)
245     * @return the list of event data
246     */
247    public List<Map<String, Object>> getEventsData(List<CalendarEvent> events, boolean fullInfo)
248    {
249        List<Map<String, Object>> result = new LinkedList<>();
250        
251        for (CalendarEvent event : events)
252        {
253            result.add(getEventData(event, fullInfo));
254        }
255        
256        return result;
257    }
258    
259    /**
260     * Get event info
261     * @param id The event id
262     * @param fullInfo true to include full info (rights, parent id, etc...)
263     * @return the event data in a map
264     */
265    @Callable
266    public Map<String, Object> getEventDataById(String id, boolean fullInfo)
267    {
268        CalendarEvent event = (CalendarEvent) _resolver.resolveById(id);
269        return getEventData(event, fullInfo);
270    }
271    
272    /**
273     * Get event info for a specific occurrence
274     * @param id The event id
275     * @param occurrence a string representing the occurrence date (ISO format).
276     * @param fullInfo true to include full info (rights, parent id, etc...)
277     * @return the event data in a map
278     */
279    @Callable
280    public Map<String, Object> getEventDataById(String id, String occurrence, boolean fullInfo)
281    {
282        CalendarEvent event = (CalendarEvent) _resolver.resolveById(id);
283        Date occurrenceDate = (Date) ParameterHelper.castValue(occurrence, ParameterType.DATE);
284        
285        return getEventData(event, occurrenceDate, fullInfo);
286    }
287    
288    /**
289     * Get event info for a specific occurrence
290     * @param event The event
291     * @param occurrenceDate the occurrence
292     * @param fullInfo true to include full info (rights, parent id, etc...)
293     * @return the event data in a map
294     */
295    public Map<String, Object> getEventData(CalendarEvent event, Date occurrenceDate, boolean fullInfo)
296    {
297        Map<String, Object> eventData = getEventData(event, fullInfo);
298        
299        if (occurrenceDate != null)
300        {
301            // replace id, start and end date with specific occurrence data
302            eventData.putAll(getEventOccurrenceData(event, occurrenceDate));
303        }
304        
305        return eventData;
306    }
307    
308    /**
309     * Get event info
310     * @param event The event
311     * @param fullInfo true to include full info (rights, parent id, etc...)
312     * @return the event data in a map
313     */
314    public Map<String, Object> getEventData(CalendarEvent event, boolean fullInfo)
315    {
316        Calendar calendar = event.getParent();
317        Map<String, Object> result = new HashMap<>();
318        
319        result.put("id", event.getId());
320        result.put("calendarId", event.getParent().getId());
321        result.put("color", calendar.getColor());
322        
323        result.put("title", event.getTitle());
324        result.put("description", event.getDescription());
325        result.put("fullDay", event.getFullDay());
326        result.put("recurrenceType", event.getRecurrenceType().toString());
327        
328        result.put("location", event.getLocation());
329        result.put("keywords", event.getKeywords());
330        
331        Date untilDate = event.getRepeatUntil();
332        if (untilDate != null)
333        {
334            result.put("untilDate", ParameterHelper.valueToString(untilDate));
335        }
336        
337        Date startDateEvent = event.getStartDate();
338        Date endDateEvent = event.getEndDate();
339        
340        if (event.getFullDay())
341        {
342            GregorianCalendar gcStart = new GregorianCalendar();
343            gcStart.setTime(startDateEvent);
344            gcStart.set(java.util.Calendar.HOUR, 0);
345            gcStart.set(java.util.Calendar.MINUTE, 0);
346            gcStart.set(java.util.Calendar.SECOND, 0);
347            gcStart.set(java.util.Calendar.MILLISECOND, 0);
348            startDateEvent = gcStart.getTime();
349            
350            GregorianCalendar gcEnd = new GregorianCalendar();
351            gcEnd.setTime(endDateEvent);
352            gcEnd.set(java.util.Calendar.HOUR, 23);
353            gcEnd.set(java.util.Calendar.MINUTE, 59);
354            gcEnd.set(java.util.Calendar.SECOND, 59);
355            gcEnd.set(java.util.Calendar.MILLISECOND, 999);
356            endDateEvent = gcEnd.getTime();
357
358            GregorianCalendar gcEndPlus1 = new GregorianCalendar();
359            gcEndPlus1.setTime(endDateEvent);
360            gcEndPlus1.add(java.util.Calendar.DAY_OF_YEAR, 1);
361            gcEndPlus1.set(java.util.Calendar.HOUR, 0);
362            gcEndPlus1.set(java.util.Calendar.MINUTE, 0);
363            gcEndPlus1.set(java.util.Calendar.SECOND, 0);
364            gcEndPlus1.set(java.util.Calendar.MILLISECOND, 0);
365
366            long milis = gcEndPlus1.getTimeInMillis() - gcStart.getTimeInMillis();
367            Integer nbDays = Math.round(milis / (1000 * 60 * 60 * 24));
368
369            result.put("nbDays", nbDays.intValue());
370            result.put("endDateNextDay", ParameterHelper.valueToString(gcEndPlus1.getTime()));
371        }
372
373        result.put("startDate", ParameterHelper.valueToString(startDateEvent));
374        result.put("endDate", ParameterHelper.valueToString(endDateEvent));
375        
376        //excluded occurences
377        List<Date> excludedOccurences = event.getExcludedOccurences();
378        if (excludedOccurences != null && !excludedOccurences.isEmpty())
379        {
380            List<String> excludedOccurencesStrings = new ArrayList<>();
381            for (Date excludedOccurence : excludedOccurences)
382            {
383                excludedOccurencesStrings.add(ParameterHelper.valueToString(excludedOccurence));
384            }
385            result.put("excludedDates", excludedOccurencesStrings);
386        }
387
388        // creator
389        UserIdentity creatorIdentity = event.getCreator();
390        User creator = _userManager.getUser(creatorIdentity);
391        
392        result.put("creator", creatorIdentity);
393        result.put("creatorFullName", creator != null ? creator.getFullName() : creatorIdentity.getLogin());
394        result.put("creationDate", ParameterHelper.valueToString(event.getCreationDate()));
395        
396        // last modification
397        UserIdentity contributorIdentity = event.getLastContributor();
398        User contributor = _userManager.getUser(contributorIdentity);
399        
400        result.put("contributor", contributorIdentity);
401        result.put("contributorFullName", contributor != null ? contributor.getFullName() : contributorIdentity.getLogin());
402        result.put("lastModified", ParameterHelper.valueToString(event.getLastModified()));
403        
404        // last validation
405        UserIdentity validatorIdentity = event.getLastValidator();
406        
407        if (validatorIdentity != null)
408        {
409            result.put("validator", validatorIdentity);
410            User validator = _userManager.getUser(validatorIdentity);
411            result.put("validatorFullName", validator != null ? validator.getFullName() : validatorIdentity.getLogin());
412        }
413        
414        Date lastValidated = event.getLastValidated();
415        if (lastValidated != null)
416        {
417            result.put("lastValidated", ParameterHelper.valueToString(lastValidated));
418        }
419        
420        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
421        long workflowId = event.getWorkflowId();
422        result.put("workflowId", workflowId);
423        result.put("workflowName", workflow.getWorkflowName(workflowId));
424        
425        List<Step> listSteps = workflow.getCurrentSteps(event.getWorkflowId());
426        if (!listSteps.isEmpty())
427        {
428            // We select the first current step
429            Step step = listSteps.get(0);
430            String stepName = _workflowHelper.getStepName(calendar.getWorkflowName(), step.getStepId());
431            String[] nameParts = stepName.split(":");
432            
433            result.put("stepId", step.getStepId());
434            result.put("stepName", nameParts[nameParts.length - 1]); // step name without the possible catalog name
435            
436            I18nizableText workflowStepName = new I18nizableText("application", stepName);
437            
438            if ("application".equals(workflowStepName.getCatalogue()))
439            {
440                result.put("icon-small-workflow", "/plugins/explorer/resources_workflow/" + workflowStepName.getKey() + "-small.png");
441            }
442            else
443            {
444                String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
445                result.put("icon-small-workflow", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png");
446            }
447        }
448        
449        if (fullInfo)
450        {
451            result.putAll(_getEventDataFullInfo(event));
452        }
453        
454        return result;
455    }
456    
457    /**
458     * Retrieves the event additional info (rights, parent id, etc...)
459     * @param event The event
460     * @return the event additional info (rights, parent id, etc...) in a map
461     */
462    protected Map<String, Object> _getEventDataFullInfo(CalendarEvent event)
463    {
464        Map<String, Object> result = new HashMap<>();
465        
466        ExplorerNode explorerNode = event.getParent();
467        ExplorerNode root = explorerNode;
468        while (true)
469        {
470            if (root.getParent() instanceof ExplorerNode)
471            {
472                root = root.getParent();
473            }
474            else
475            {
476                break;
477            }
478        }
479        result.put("rootId", root.getId());
480        result.put("parentId", explorerNode.getId());
481        result.put("name", event.getName());
482        result.put("path", explorerNode.getExplorerPath());
483        result.put("isModifiable", true);
484        
485        result.put("rights", _getUserRights(explorerNode));
486        
487        return result;
488    }
489    
490    /**
491     * Get the user rights on the resource collection
492     * @param node The explorer node
493     * @return The user's rights
494     */
495    protected Set<String> _getUserRights(ExplorerNode node)
496    {
497        return _rightManager.getUserRights(_currentUserProvider.getUser(), node);
498    }
499    
500    /**
501     * Get info about the occurrence of an event
502     * @param event The event
503     * @param date The start date of the occurrence of the event
504     * @return the event occurrence data in a map
505     */
506    public Map<String, Object> getEventOccurrenceData(CalendarEvent event, Date date)
507    {
508        Map<String, Object> result = new HashMap<>();
509        
510        String occurrenceDateIso = ParameterHelper.valueToString(date);
511        result.put("id", event.getId() + "$" + occurrenceDateIso);
512        result.put("occurrenceDate", occurrenceDateIso);
513        
514        Date firstStartDateEvent = event.getStartDate();
515        Date firstEndDateEvent = event.getEndDate();
516        
517        long diff = firstEndDateEvent.getTime() - firstStartDateEvent.getTime();
518        Date startDateEvent = date;
519        Date endDateEvent = new Date(date.getTime() + diff);
520        
521        if (event.getFullDay())
522        {
523            GregorianCalendar gcStart = new GregorianCalendar();
524            gcStart.setTime(startDateEvent);
525            gcStart.set(java.util.Calendar.HOUR_OF_DAY, 0);
526            gcStart.set(java.util.Calendar.MINUTE, 0);
527            gcStart.set(java.util.Calendar.SECOND, 0);
528            gcStart.set(java.util.Calendar.MILLISECOND, 0);
529            
530            startDateEvent = gcStart.getTime();
531            
532            GregorianCalendar gcEnd = new GregorianCalendar();
533            gcEnd.setTime(endDateEvent);
534            gcEnd.set(java.util.Calendar.HOUR_OF_DAY, 23);
535            gcEnd.set(java.util.Calendar.MINUTE, 59);
536            gcEnd.set(java.util.Calendar.SECOND, 59);
537            gcEnd.set(java.util.Calendar.MILLISECOND, 999);
538            
539            endDateEvent = gcEnd.getTime();
540        }
541        
542        result.put("startDate", ParameterHelper.valueToString(startDateEvent));
543        result.put("endDate", ParameterHelper.valueToString(endDateEvent));
544        
545        return result;
546    }
547    
548    /**
549     * Add a calendar
550     * @param id The identifier of the parent in which the calendar will be added
551     * @param inputName The desired name for the calendar
552     * @param description The calendar description
553     * @param templateDesc The calendar template description
554     * @param color The calendar color
555     * @param visibility The calendar visibility
556     * @param workflowName The calendar workflow name
557     * @param renameIfExists True to rename if existing
558     * @return The result map with id, parentId and name keys
559     * @throws IllegalAccessException If the user has no sufficient rights
560     */
561    @Callable
562    public Map<String, Object> addCalendar(String id, String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists) throws IllegalAccessException
563    {
564        Map<String, Object> result = new HashMap<>();
565        
566        String originalName = Text.escapeIllegalJcrChars(inputName);
567        assert id != null;
568        
569        AmetysObject object = _resolver.resolveById(id);
570        if (!(object instanceof ModifiableResourceCollection || object instanceof Calendar))
571        {
572            throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass());
573        }
574        
575        ModifiableTraversableAmetysObject parent = (ModifiableTraversableAmetysObject) object;
576        
577        // Check user right
578        _explorerResourcesDAO.checkUserRight(object, RIGHTS_CALENDAR_ADD);
579        
580        if (BooleanUtils.isNotTrue(renameIfExists) && parent.hasChild(originalName))
581        {
582            getLogger().warn("Cannot create the calendar with name '" + originalName + "', an object with same name already exists.");
583            result.put("message", "already-exist");
584            return result;
585        }
586        
587        if (!_explorerResourcesDAO.checkLock(parent))
588        {
589            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + object.getName() + "' but it is locked by another user");
590            result.put("message", "locked");
591            return result;
592        }
593        
594        int index = 2;
595        String name = originalName;
596        while (parent.hasChild(name))
597        {
598            name = originalName + " (" + index + ")";
599            index++;
600        }
601        
602        JCRCalendar calendar = parent.createChild(name, JCRCalendarFactory.CALENDAR_NODETYPE);
603        calendar.setWorkflowName(workflowName);
604        calendar.setDescription(description);
605        calendar.setTemplateDescription(templateDesc);
606        calendar.setColor(color);
607        calendar.setVisibility(StringUtils.isNotEmpty(visibility) ? CalendarVisibility.valueOf(visibility.toUpperCase()) : CalendarVisibility.PRIVATE);
608        parent.saveChanges();
609        
610        // Notify listeners
611        Map<String, Object> eventParams = new HashMap<>();
612        eventParams.put(ObservationConstants.ARGS_ID, calendar.getId());
613        eventParams.put(ObservationConstants.ARGS_PARENT_ID, id);
614        eventParams.put(ObservationConstants.ARGS_NAME, name);
615        eventParams.put(ObservationConstants.ARGS_PATH, calendar.getPath());
616        
617        _observationManager.notify(new Event(ObservationConstants.EVENT_CALENDAR_CREATED, _currentUserProvider.getUser(), eventParams));
618        
619        result.put("id", calendar.getId());
620        result.put("parentId", id);
621        result.put("name", Text.unescapeIllegalJcrChars(name));
622        
623        return result;
624    }
625    
626    /**
627     * Edit a calendar
628     * @param id The identifier of the calendar
629     * @param inputName The new name
630     * @param description The new description
631     * @param templateDesc The new calendar template description
632     * @param color The calendar color
633     * @param visibility The calendar visibility
634     * @param workflowName The calendar workflow name
635     * @param renameIfExists True to rename if existing
636     * @param fullEdit true to allow full edition, otherwise only the name will be changed
637     * @return The result map with id and name keys
638     * @throws IllegalAccessException If the user has no sufficient rights
639     */
640    @Callable
641    public Map<String, Object> editCalendar(String id, String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists, Boolean fullEdit) throws IllegalAccessException
642    {
643        // TODO handle template desc
644        Map<String, Object> result = new HashMap<>();
645        
646        assert id != null;
647        String rename = Text.escapeIllegalJcrChars(inputName);
648        
649        AmetysObject object = _resolver.resolveById(id);
650        if (!(object instanceof ModifiableCalendar))
651        {
652            throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
653        }
654        
655        ModifiableCalendar calendar = (ModifiableCalendar) object;
656        
657        // Check user right
658        _explorerResourcesDAO.checkUserRight(object, RIGHTS_CALENDAR_EDIT);
659        
660        String name = calendar.getName();
661        ModifiableTraversableAmetysObject parent = calendar.getParent();
662        
663        if (BooleanUtils.isNotTrue(renameIfExists) && !StringUtils.equals(rename, name) && parent.hasChild(rename))
664        {
665            getLogger().warn("Cannot edit the calendar with the new name '" + inputName + "', an object with same name already exists.");
666            result.put("message", "already-exist");
667            return result;
668        }
669        
670        if (!_explorerResourcesDAO.checkLock(calendar))
671        {
672            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify calendar '" + object.getName() + "' but it is locked by another user");
673            result.put("message", "locked");
674            return result;
675        }
676        
677        if (!StringUtils.equals(name, rename))
678        {
679            int index = 2;
680            name = Text.escapeIllegalJcrChars(rename);
681            while (parent.hasChild(name))
682            {
683                name = rename + " (" + index + ")";
684                index++;
685            }
686            calendar.rename(name);
687        }
688        
689        if (BooleanUtils.isTrue(fullEdit))
690        {
691            calendar.setDescription(description);
692            calendar.setTemplateDescription(templateDesc);
693            calendar.setColor(color);
694            calendar.setVisibility(StringUtils.isNotEmpty(visibility) ? CalendarVisibility.valueOf(visibility.toUpperCase()) : CalendarVisibility.PRIVATE);
695        }
696        
697        parent.saveChanges();
698        
699        // Notify listeners
700        Map<String, Object> eventParams = new HashMap<>();
701        eventParams.put(ObservationConstants.ARGS_ID, calendar.getId());
702        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
703        eventParams.put(ObservationConstants.ARGS_NAME, name);
704        eventParams.put(ObservationConstants.ARGS_PATH, calendar.getPath());
705        
706        _observationManager.notify(new Event(ObservationConstants.EVENT_CALENDAR_UPDATED, _currentUserProvider.getUser(), eventParams));
707        
708        result.put("id", calendar.getId());
709        result.put("title", Text.unescapeIllegalJcrChars(name));
710
711        return result;
712    }
713    
714    /**
715     * Move a event to another calendar
716     * @param event The event to move
717     * @param parent The new parent calendar
718     * @throws AmetysRepositoryException if an error occurred while moving
719     */
720    public void move(JCRCalendarEvent event, JCRCalendar parent) throws AmetysRepositoryException
721    {
722        try
723        {
724            event.getNode().getSession().move(event.getNode().getPath(), parent.getNode().getPath() + "/ametys:calendar-event");
725            
726            Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event);
727
728            String previousWorkflowName = workflow.getWorkflowName(event.getWorkflowId());
729            String workflowName = parent.getWorkflowName();
730
731            if (!StringUtils.equals(previousWorkflowName, workflowName))
732            {
733                // If both calendar have a different workflow, initialize a new workflow instance for the event
734                HashMap<String, Object> inputs = new HashMap<>();
735                inputs.put(AbstractExplorerNodeWorkflowComponent.EXPLORERNODE_KEY, parent);
736                workflow = _workflowProvider.getAmetysObjectWorkflow(event);
737                
738                long workflowId = workflow.initialize(workflowName, 0, inputs);
739                event.setWorkflowId(workflowId);
740            }
741        }
742        catch (WorkflowException | RepositoryException e)
743        {
744            String errorMsg = String.format("Fail to move the event '%s' to the calendar '%s'.", event.getId(), parent.getId());
745            throw new AmetysRepositoryException(errorMsg, e);
746        }
747    }
748    
749    /**
750     * Delete a calendar
751     * @param id The id of the calendar
752     * @return The result map with id, parent id and message keys
753     * @throws IllegalAccessException If the user has no sufficient rights
754     */
755    @Callable
756    public Map<String, Object> deleteCalendar(String id) throws IllegalAccessException
757    {
758        Map<String, Object> result = new HashMap<>();
759
760        assert id != null;
761        
762        AmetysObject object = _resolver.resolveById(id);
763        if (!(object instanceof ModifiableCalendar))
764        {
765            throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
766        }
767        
768        ModifiableCalendar calendar = (ModifiableCalendar) object;
769        
770        // Check user right
771        _explorerResourcesDAO.checkUserRight(object, RIGHTS_CALENDAR_DELETE);
772        
773        if (!_explorerResourcesDAO.checkLock(calendar))
774        {
775            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete calendar'" + object.getName() + "' but it is locked by another user");
776            result.put("message", "locked");
777            return result;
778        }
779        
780        ModifiableExplorerNode parent = calendar.getParent();
781        String parentId = parent.getId();
782        String name = calendar.getName();
783        String path = calendar.getPath();
784        
785        calendar.remove();
786        parent.saveChanges();
787     
788        // Notify listeners
789        Map<String, Object> eventParams = new HashMap<>();
790        eventParams.put(ObservationConstants.ARGS_ID, id);
791        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId);
792        eventParams.put(ObservationConstants.ARGS_NAME, name);
793        eventParams.put(ObservationConstants.ARGS_PATH, path);
794        
795        _observationManager.notify(new Event(ObservationConstants.EVENT_CALENDAR_DELETED, _currentUserProvider.getUser(), eventParams));
796        
797        result.put("id", id);
798        result.put("parentId", parentId);
799        
800        return result;
801    }
802    
803    /**
804     * Do an event workflow action
805     * @param parameters The map of action parameters
806     * @return The map of results populated by the workflow action
807     * @throws WorkflowException if an error occurred
808     */
809    @Callable
810    public Map<String, Object> doWorkflowEventAction(Map<String, Object> parameters) throws WorkflowException
811    {
812        Map<String, Object> result = new HashMap<>();
813        HashMap<String, Object> inputs = new HashMap<>();
814
815        inputs.put("parameters", parameters);
816        inputs.put("result", result);
817        
818        String eventId = (String) parameters.get("id");
819        Long workflowInstanceId = null;
820        CalendarEvent event = null;
821        if (StringUtils.isNotEmpty(eventId))
822        {
823            event = _resolver.resolveById(eventId);
824            workflowInstanceId = event.getWorkflowId();
825        }
826        
827        inputs.put("eventId", eventId);
828        
829        Calendar calendar = null; 
830        String calendarId = (String) parameters.get("parentId");
831        
832        if (StringUtils.isNotEmpty(calendarId))
833        {
834            calendar = _resolver.resolveById(calendarId);
835        }
836        // parentId can be not provided for some basic actions where the event already exists
837        else if (event != null)
838        {
839            calendar = event.getParent();
840        }
841        else
842        {
843            throw new WorkflowException("Unable to retrieve the current calendar");
844        }
845        
846        inputs.put(AbstractExplorerNodeWorkflowComponent.EXPLORERNODE_KEY, calendar);
847        
848        String workflowName = calendar.getWorkflowName();
849        if (workflowName == null)
850        {
851            throw new IllegalArgumentException("The workflow name is not specified");
852        }
853        
854        int actionId =  (int) parameters.get("actionId");
855        
856        boolean sendMail = true;
857        String choice = (String) parameters.get("choice");
858        if (actionId == 2 && "unit".equals(choice))
859        {
860            sendMail = false;
861        }
862        inputs.put("sendMail", sendMail);
863        
864        Workflow workflow = _workflowProvider.getAmetysObjectWorkflow(event != null ? event : null);
865        
866        if (workflowInstanceId == null)
867        {
868            try
869            {
870                workflow.initialize(workflowName, actionId, inputs);
871            }   
872            catch (WorkflowException e)
873            {
874                getLogger().error("An error occured while creating workflow '" + workflowName + "' with action '" + actionId, e);
875                throw e;
876            }
877        }
878        else
879        {
880            try
881            {
882                workflow.doAction(workflowInstanceId, actionId, inputs);
883            }
884            catch (WorkflowException e)
885            {
886                getLogger().error("An error occured while doing action '" + actionId + "'with the workflow '" + workflowName, e);
887                throw e;
888            }
889        }
890        
891        return result;
892    }
893    
894    /**
895     * Delete an event
896     * @param id The id of the event
897     * @param occurrence a string representing the occurrence date (ISO format).
898     * @param choice The type of modification
899     * @return The result map with id, parent id and message keys
900     * @throws IllegalAccessException If the user has no sufficient rights
901     */
902    @Callable
903    public Map<String, Object> deleteEvent(String id, String occurrence, String choice) throws IllegalAccessException
904    {
905        Map<String, Object> result = new HashMap<>();
906
907        assert id != null;
908        
909        AmetysObject object = _resolver.resolveById(id);
910        if (!(object instanceof ModifiableCalendarEvent))
911        {
912            throw new IllegalClassException(ModifiableCalendarEvent.class, object.getClass());
913        }
914        
915        ModifiableCalendarEvent event = (ModifiableCalendarEvent) object;
916        ModifiableCalendar calendar = event.getParent();
917        
918        // Check user right
919        try
920        {
921            _explorerResourcesDAO.checkUserRight(calendar, RIGHTS_EVENT_DELETE);
922        }
923        catch (IllegalAccessException e)
924        {
925            UserIdentity user = _currentUserProvider.getUser();
926            UserIdentity creator = event.getCreator();
927            RightResult rightCreator = _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendar);
928            boolean hasOwnDeleteRight = rightCreator == RightResult.RIGHT_ALLOW && creator.equals(user);
929            if (!hasOwnDeleteRight)
930            {
931                // rethrow exception
932                throw e;
933            }
934        }
935        
936        if (!_explorerResourcesDAO.checkLock(event))
937        {
938            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete event'" + object.getName() + "' but it is locked by another user");
939            result.put("message", "locked");
940            return result;
941        }
942        
943        String parentId = calendar.getId();
944        String name = event.getName();
945        String path = event.getPath();
946        
947        if (StringUtils.isNotBlank(choice) && choice.equals("unit"))
948        {
949            ArrayList<Date> excludedOccurrences = new ArrayList<>();
950            excludedOccurrences.addAll(event.getExcludedOccurences());
951            long date = ZonedDateTime.parse(occurrence, DateTimeFormatter.ISO_DATE_TIME).toInstant().toEpochMilli();
952            GregorianCalendar gCalendar = new GregorianCalendar();
953            gCalendar.setTimeInMillis(date);
954            gCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
955            gCalendar.set(java.util.Calendar.MINUTE, 0);
956            gCalendar.set(java.util.Calendar.SECOND, 0);
957            gCalendar.set(java.util.Calendar.MILLISECOND, 0);
958            
959            Date dateExcluded = gCalendar.getTime();
960            excludedOccurrences.add(dateExcluded);
961            
962            event.setExcludedOccurrences(excludedOccurrences);
963        }
964        else
965        {
966            event.remove();
967        }
968        
969        calendar.saveChanges();
970        
971        // Notify listeners
972        Map<String, Object> eventParams = new HashMap<>();
973        eventParams.put(ObservationConstants.ARGS_CALENDAR, calendar);
974        eventParams.put(ObservationConstants.ARGS_ID, id);
975        eventParams.put(ObservationConstants.ARGS_NAME, name);
976        eventParams.put(ObservationConstants.ARGS_PATH, path);
977        _observationManager.notify(new Event(ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, _currentUserProvider.getUser(), eventParams));
978        
979        result.put("id", id);
980        result.put("parentId", parentId);
981        
982        return result;
983    }
984}
985