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