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.workspaces.calendars;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.UUID;
024
025import org.apache.avalon.framework.service.ServiceException;
026import org.apache.avalon.framework.service.ServiceManager;
027import org.apache.commons.lang.BooleanUtils;
028import org.apache.commons.lang.IllegalClassException;
029import org.apache.commons.lang3.StringUtils;
030import org.apache.jackrabbit.util.Text;
031
032import org.ametys.core.observation.Event;
033import org.ametys.core.right.RightManager.RightResult;
034import org.ametys.core.ui.Callable;
035import org.ametys.core.user.UserIdentity;
036import org.ametys.plugins.explorer.ModifiableExplorerNode;
037import org.ametys.plugins.explorer.ObservationConstants;
038import org.ametys.plugins.repository.AmetysObject;
039import org.ametys.plugins.repository.AmetysObjectIterable;
040import org.ametys.plugins.repository.AmetysObjectIterator;
041import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
042import org.ametys.plugins.repository.query.QueryHelper;
043import org.ametys.plugins.repository.query.expression.Expression;
044import org.ametys.plugins.repository.query.expression.Expression.Operator;
045import org.ametys.plugins.repository.query.expression.StringExpression;
046import org.ametys.plugins.workspaces.calendars.Calendar.CalendarVisibility;
047import org.ametys.plugins.workspaces.calendars.events.CalendarEvent;
048import org.ametys.plugins.workspaces.calendars.events.CalendarEventJSONHelper;
049import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendar;
050import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarFactory;
051import org.ametys.plugins.workspaces.project.ProjectConstants;
052
053/**
054 * Calendar DAO
055 */
056public class CalendarDAO extends AbstractCalendarDAO
057{
058    /** Avalon Role */
059    public static final String ROLE = CalendarDAO.class.getName();
060
061    /** The tasks list JSON helper */
062    protected CalendarEventJSONHelper _calendarEventJSONHelper;
063
064    @Override
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        super.service(manager);
068        _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE);
069    }
070        
071    /**
072     * Get calendar info
073     * @param calendar The calendar
074     * @param recursive True to get data for sub calendars
075     * @param includeEvents True to also include child events
076     * @param useICSFormat true to use ICS Format for dates
077     * @return the calendar data in a map
078     */
079    public Map<String, Object> getCalendarData(Calendar calendar, boolean recursive, boolean includeEvents, boolean useICSFormat)
080    {
081        Map<String, Object> result = new HashMap<>();
082        
083        result.put("id", calendar.getId());
084        result.put("title", Text.unescapeIllegalJcrChars(calendar.getName()));
085        result.put("description", calendar.getDescription());
086        result.put("templateDesc", calendar.getTemplateDescription());
087        result.put("color", calendar.getColor());
088        result.put("visibility", calendar.getVisibility().name().toLowerCase());
089        result.put("public", calendar.getVisibility() == CalendarVisibility.PUBLIC);
090        result.put("workflowName", calendar.getWorkflowName());
091        
092        if (recursive)
093        {
094            List<Map<String, Object>> calendarList = new LinkedList<>();
095            result.put("calendars", calendarList);
096            AmetysObjectIterable<AmetysObject> children = calendar.getChildren();
097            for (AmetysObject child : children)
098            {
099                if (child instanceof Calendar)
100                {
101                    calendarList.add(getCalendarData((Calendar) child, recursive, includeEvents, useICSFormat));
102                }
103            }
104        }
105        
106        if (includeEvents)
107        {
108            List<Map<String, Object>> eventList = new LinkedList<>();
109            result.put("events", eventList);
110            
111            AmetysObjectIterable<AmetysObject> children = calendar.getChildren();
112            for (AmetysObject child : children)
113            {
114                if (child instanceof CalendarEvent)
115                {
116                    eventList.add(_calendarEventJSONHelper.eventAsJson((CalendarEvent) child, false, useICSFormat));
117                }
118            }
119        }
120
121        result.put("rights", _extractCalendarRightData(calendar));
122        result.put("token", getCalendarIcsToken(calendar, true));
123        
124        
125        return result;
126    }
127    
128    /**
129     * Get calendar info
130     * @param calendar The calendar
131     * @return the calendar data in a map
132     */
133    public Map<String, Object> getCalendarProperties(Calendar calendar)
134    {
135        return getCalendarData(calendar, false, false, false);
136    }
137    /**
138     * Add a calendar
139     * @param inputName The desired name for the calendar
140     * @param color The calendar color
141     * @param isPublic true if the calendar is public
142     * @return The result map with id, parentId and name keys
143     * @throws IllegalAccessException If the user has no sufficient rights
144     */
145    @Callable
146    public Map<String, Object> addCalendar(String inputName, String color, boolean isPublic) throws IllegalAccessException
147    {
148        String rootId = _getCalendarRoot().getId();
149        return addCalendar(rootId, inputName, StringUtils.EMPTY, StringUtils.EMPTY, color, isPublic ? CalendarVisibility.PUBLIC.name() : CalendarVisibility.PRIVATE.name(), "calendar-default", false);
150    }
151    
152    /**
153     * Add a calendar
154     * @param id The identifier of the parent in which the calendar will be added
155     * @param inputName The desired name for the calendar
156     * @param description The calendar description
157     * @param templateDesc The calendar template description
158     * @param color The calendar color
159     * @param visibility The calendar visibility
160     * @param workflowName The calendar workflow name
161     * @param renameIfExists True to rename if existing
162     * @return The result map with id, parentId and name keys
163     * @throws IllegalAccessException If the user has no sufficient rights
164     */
165    public Map<String, Object> addCalendar(String id, String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists) throws IllegalAccessException
166    {
167        return addCalendar(_resolver.resolveById(id), inputName, description, templateDesc, color, visibility, workflowName, renameIfExists, true, true);
168    }
169        
170    /**
171     * Add a calendar
172     * @param parent The parent in which the calendar will be added
173     * @param inputName The desired name for the calendar
174     * @param description The calendar description
175     * @param templateDesc The calendar template description
176     * @param color The calendar color
177     * @param visibility The calendar visibility
178     * @param workflowName The calendar workflow name
179     * @param renameIfExists True to rename if existing
180     * @param checkRights true to check if the current user have enough rights to create the calendar
181     * @param notify True to notify the calendar creation
182     * @return The result map with id, parentId and name keys
183     * @throws IllegalAccessException If the user has no sufficient rights
184     */
185    public Map<String, Object> addCalendar(ModifiableTraversableAmetysObject parent, String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists, Boolean checkRights, boolean notify) throws IllegalAccessException
186    {
187        String originalName = Text.escapeIllegalJcrChars(inputName);
188        
189        // Check user right
190        if (checkRights)
191        {
192            _explorerResourcesDAO.checkUserRight(parent, RIGHTS_CALENDAR_ADD);
193        }
194        
195        if (BooleanUtils.isNotTrue(renameIfExists) && parent.hasChild(originalName))
196        {
197            getLogger().warn("Cannot create the calendar with name '" + originalName + "', an object with same name already exists.");
198            return Map.of("message", "already-exist");
199        }
200        
201        if (!_explorerResourcesDAO.checkLock(parent))
202        {
203            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify the object '" + parent.getName() + "' but it is locked by another user");
204            return Map.of("message", "locked");
205        }
206        
207        int index = 2;
208        String name = originalName;
209        while (parent.hasChild(name))
210        {
211            name = originalName + " (" + index + ")";
212            index++;
213        }
214        
215        JCRCalendar calendar = parent.createChild(name, JCRCalendarFactory.CALENDAR_NODETYPE);
216        calendar.setWorkflowName(workflowName);
217        calendar.setDescription(description);
218        calendar.setTemplateDescription(templateDesc);
219        calendar.setColor(color);
220        calendar.setVisibility(StringUtils.isNotEmpty(visibility) ? CalendarVisibility.valueOf(visibility.toUpperCase()) : CalendarVisibility.PRIVATE);
221        parent.saveChanges();
222        
223        // Notify listeners
224        Map<String, Object> eventParams = new HashMap<>();
225        eventParams.put(ObservationConstants.ARGS_ID, calendar.getId());
226        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
227        eventParams.put(ObservationConstants.ARGS_NAME, name);
228        eventParams.put(ObservationConstants.ARGS_PATH, calendar.getPath());
229        
230        if (notify)
231        {
232            _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_CREATED, _currentUserProvider.getUser(), eventParams));
233        }
234        
235        return getCalendarProperties(calendar);
236    }
237
238    /**
239     * Edit a calendar
240     * @param id The identifier of the calendar
241     * @param inputName The new name
242     * @param templateDesc The new calendar template description
243     * @param color The calendar color
244     * @param isPublic true if the calendar is public
245     * @return The result map with id and name keys
246     * @throws IllegalAccessException If the user has no sufficient rights
247     */
248    @Callable
249    public Map<String, Object> editCalendar(String id, String inputName, String templateDesc, String color, boolean isPublic) throws IllegalAccessException
250    {
251        CalendarVisibility visibility = isPublic ? CalendarVisibility.PUBLIC : CalendarVisibility.PRIVATE;
252        
253        assert id != null;
254        String rename = Text.escapeIllegalJcrChars(inputName);
255        
256        AmetysObject object = _resolver.resolveById(id);
257        if (!(object instanceof ModifiableCalendar))
258        {
259            throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
260        }
261        
262        ModifiableCalendar calendar = (ModifiableCalendar) object;
263        
264        // Check user right
265        _explorerResourcesDAO.checkUserRight(object, RIGHTS_CALENDAR_EDIT);
266        
267        String name = calendar.getName();
268        ModifiableTraversableAmetysObject parent = calendar.getParent();
269        
270        if (!StringUtils.equals(rename, name) && parent.hasChild(rename))
271        {
272            getLogger().warn("Cannot edit the calendar with the new name '" + inputName + "', an object with same name already exists.");
273            return Map.of("message", "already-exist");
274        }
275        
276        if (!_explorerResourcesDAO.checkLock(calendar))
277        {
278            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify calendar '" + object.getName() + "' but it is locked by another user");
279            return Map.of("message", "locked");
280        }
281        
282        if (!StringUtils.equals(name, rename))
283        {
284            int index = 2;
285            name = Text.escapeIllegalJcrChars(rename);
286            while (parent.hasChild(name))
287            {
288                name = rename + " (" + index + ")";
289                index++;
290            }
291            calendar.rename(name);
292        }
293        
294        calendar.setTemplateDescription(templateDesc);
295        calendar.setColor(color);
296        calendar.setVisibility(visibility);
297        
298        parent.saveChanges();
299        
300        // Notify listeners
301        Map<String, Object> eventParams = new HashMap<>();
302        eventParams.put(ObservationConstants.ARGS_ID, calendar.getId());
303        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId());
304        eventParams.put(ObservationConstants.ARGS_NAME, name);
305        eventParams.put(ObservationConstants.ARGS_PATH, calendar.getPath());
306        
307        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_UPDATED, _currentUserProvider.getUser(), eventParams));
308
309        return getCalendarProperties(calendar);
310    }
311    
312    /**
313     * Delete a calendar
314     * @param id The id of the calendar
315     * @return The result map with id, parent id and message keys
316     * @throws IllegalAccessException If the user has no sufficient rights
317     */
318    @Callable
319    public Map<String, Object> deleteCalendar(String id) throws IllegalAccessException
320    {
321        Map<String, Object> result = new HashMap<>();
322
323        assert id != null;
324        
325        AmetysObject object = _resolver.resolveById(id);
326        if (!(object instanceof ModifiableCalendar))
327        {
328            throw new IllegalClassException(ModifiableCalendar.class, object.getClass());
329        }
330        
331        ModifiableCalendar calendar = (ModifiableCalendar) object;
332        
333        // Check user right
334        _explorerResourcesDAO.checkUserRight(object, RIGHTS_CALENDAR_DELETE);
335        
336        if (!_explorerResourcesDAO.checkLock(calendar))
337        {
338            getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to delete calendar'" + object.getName() + "' but it is locked by another user");
339            result.put("message", "locked");
340            return result;
341        }
342        
343        ModifiableExplorerNode parent = calendar.getParent();
344        String parentId = parent.getId();
345        String name = calendar.getName();
346        String path = calendar.getPath();
347        
348        calendar.remove();
349        parent.saveChanges();
350     
351        // Notify listeners
352        Map<String, Object> eventParams = new HashMap<>();
353        eventParams.put(ObservationConstants.ARGS_ID, id);
354        eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId);
355        eventParams.put(ObservationConstants.ARGS_NAME, name);
356        eventParams.put(ObservationConstants.ARGS_PATH, path);
357        
358        _observationManager.notify(new Event(org.ametys.plugins.workspaces.calendars.ObservationConstants.EVENT_CALENDAR_DELETED, _currentUserProvider.getUser(), eventParams));
359        
360        result.put("id", id);
361        result.put("parentId", parentId);
362        
363        return result;
364    }
365        
366    /**
367     * Get or create the calendar ICS token
368     * @param calendar The calendar
369     * @param createIfNotExisting Create the token if none exists for the given calendar
370     * @return The token
371     */
372    public String getCalendarIcsToken(Calendar calendar, boolean createIfNotExisting)
373    {
374        String token = calendar.getIcsUrlToken();
375        
376        if (createIfNotExisting && token == null && calendar instanceof ModifiableCalendar)
377        {
378            token = UUID.randomUUID().toString();
379            ((ModifiableCalendar) calendar).setIcsUrlToken(token);
380            ((ModifiableCalendar) calendar).saveChanges();
381        }
382        
383        return token;
384    }
385    
386    /**
387     * Retrieve the calendar for the matching ICS token
388     * @param token The ICS token
389     * @return The calendar, or null if not found
390     */
391    public Calendar getCalendarFromIcsToken(String token)
392    {
393        if (StringUtils.isEmpty(token))
394        {
395            return null;
396        }
397        
398        Expression expr = new StringExpression(JCRCalendar.CALENDAR_ICS_TOKEN, Operator.EQ, token);
399        String calendarsQuery = QueryHelper.getXPathQuery(null, JCRCalendarFactory.CALENDAR_NODETYPE, expr);
400        AmetysObjectIterable<Calendar> calendars = _resolver.query(calendarsQuery);
401        AmetysObjectIterator<Calendar> calendarsIterator = calendars.iterator();
402        
403        if (calendarsIterator.getSize() > 0)
404        {
405            return calendarsIterator.next();
406        }
407
408        return null;
409    }
410    
411    /**
412     * Internal method to extract the data concerning the right of the current user for a calendar
413     * @param calendar The calendar
414     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
415     */
416    protected  Map<String, Object> _extractCalendarRightData(Calendar calendar)
417    {
418        Map<String, Object> rightsData = new HashMap<>();
419        UserIdentity user = _currentUserProvider.getUser();
420        
421        // Add
422        rightsData.put("add-event", _rightManager.hasRight(user, RIGHTS_EVENT_ADD, calendar) == RightResult.RIGHT_ALLOW);
423        
424        // edit - delete
425        rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_CALENDAR_EDIT, calendar) == RightResult.RIGHT_ALLOW);
426        rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_CALENDAR_DELETE, calendar) == RightResult.RIGHT_ALLOW);
427        
428        return rightsData;
429    }
430    
431    /**
432     * Get the data of every available calendar for the current project
433     * @return the list of calendars
434     */
435    @Callable
436    public List<Map<String, Object>> getCalendars()
437    {
438        List<Map<String, Object>> calendarsData = new ArrayList<>();
439        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
440        
441        AmetysObjectIterable<Calendar> calendars = calendarModule.getCalendars(_getProject());
442        if (calendars != null)
443        {
444            for (Calendar calendar : calendars)
445            {
446                if (calendarModule.canView(calendar))
447                {
448                    calendarsData.add(this.getCalendarProperties(calendar));
449                }
450            }
451        }
452        
453        return calendarsData;
454    }
455
456    /**
457     * Get the colors of calendars
458     * @return colors
459     */
460    @Callable
461    public Map<String, CalendarColorsComponent.CalendarColor> getColors()
462    {
463        return _calendarColors.getColors();
464    }
465    
466    /**
467     * Get user rights from project name
468     * @return the user rights
469     */
470    @Callable
471    public Map<String, Object> getUserRights()
472    {
473        Map<String, Object> results = new HashMap<>();
474        ModifiableTraversableAmetysObject calendarRoot = _getCalendarRoot();
475        
476        UserIdentity user = _currentUserProvider.getUser();
477        results.put("canCreateCalendar", _rightManager.hasRight(user, RIGHTS_CALENDAR_ADD, calendarRoot) == RightResult.RIGHT_ALLOW);
478        results.put("canEditCalendar", _rightManager.hasRight(user, RIGHTS_CALENDAR_EDIT, calendarRoot) == RightResult.RIGHT_ALLOW);
479        results.put("canRemoveCalendar", _rightManager.hasRight(user, RIGHTS_CALENDAR_DELETE, calendarRoot) == RightResult.RIGHT_ALLOW);
480        results.put("canCreateEvent", _rightManager.hasRight(user, RIGHTS_EVENT_ADD, calendarRoot) == RightResult.RIGHT_ALLOW);
481        results.put("canEditEvent", _rightManager.hasRight(user, RIGHTS_EVENT_EDIT, calendarRoot) == RightResult.RIGHT_ALLOW);
482        results.put("canRemoveAnyEvent", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE, calendarRoot) == RightResult.RIGHT_ALLOW);
483        results.put("canRemoveSelfEvent", _rightManager.hasRight(user, RIGHTS_EVENT_DELETE_OWN, calendarRoot) == RightResult.RIGHT_ALLOW);
484        results.put("canCreateTags", _rightManager.hasRight(user, ProjectConstants.RIGHT_PROJECT_ADD_TAG, calendarRoot) == RightResult.RIGHT_ALLOW);
485        results.put("canHandleResource", _rightManager.hasRight(user, RIGHTS_HANDLE_RESOURCE, calendarRoot) == RightResult.RIGHT_ALLOW);
486        results.put("canBookResource", _rightManager.hasRight(user, RIGHTS_BOOK_RESOURCE, calendarRoot) == RightResult.RIGHT_ALLOW);
487        results.put("sharePrivateCalendar", _rightManager.hasRight(user, RIGHTS_EVENT_EDIT, calendarRoot) == RightResult.RIGHT_ALLOW);
488        
489        return results;
490    }
491
492    /**
493     * Get the calendar root
494     * @return the calendar root
495     */
496    protected ModifiableTraversableAmetysObject _getCalendarRoot()
497    {
498        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
499        return calendarModule.getCalendarsRoot(_getProject(), true);
500    }
501    
502    /**
503     * Get the data of calendar used to store resources
504     * @return the calendar used to store resources
505     */
506    @Callable
507    public Map<String, Object> getResourceCalendar()
508    {
509        CalendarWorkspaceModule calendarModule = (CalendarWorkspaceModule) _workspaceModuleEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
510        
511        Calendar calendar = calendarModule.getResourceCalendar(_getProject());
512        return this.getCalendarProperties(calendar);
513    }
514}