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