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