001/*
002 *  Copyright 2016 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.text.DateFormat;
019import java.text.SimpleDateFormat;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034import org.apache.commons.collections.ListUtils;
035import org.apache.commons.lang.IllegalClassException;
036import org.apache.commons.lang.time.DateUtils;
037import org.apache.commons.lang3.StringUtils;
038
039import org.ametys.cms.transformation.xslt.ResolveURIComponent;
040import org.ametys.core.right.RightManager.RightResult;
041import org.ametys.core.ui.Callable;
042import org.ametys.core.user.UserIdentity;
043import org.ametys.plugins.explorer.ExplorerNode;
044import org.ametys.plugins.explorer.calendars.Calendar;
045import org.ametys.plugins.explorer.calendars.Calendar.CalendarVisibility;
046import org.ametys.plugins.explorer.calendars.CalendarEvent;
047import org.ametys.plugins.explorer.calendars.actions.CalendarDAO;
048import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEvent;
049import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEventFactory;
050import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
051import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
052import org.ametys.plugins.repository.AmetysObject;
053import org.ametys.plugins.repository.AmetysObjectIterable;
054import org.ametys.plugins.repository.AmetysObjectIterator;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
057import org.ametys.plugins.repository.UnknownAmetysObjectException;
058import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
059import org.ametys.plugins.repository.query.QueryHelper;
060import org.ametys.plugins.repository.query.SortCriteria;
061import org.ametys.plugins.repository.query.expression.AndExpression;
062import org.ametys.plugins.repository.query.expression.DateExpression;
063import org.ametys.plugins.repository.query.expression.Expression;
064import org.ametys.plugins.repository.query.expression.Expression.Operator;
065import org.ametys.plugins.repository.query.expression.OrExpression;
066import org.ametys.plugins.repository.query.expression.StringExpression;
067import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
068import org.ametys.plugins.workspaces.project.objects.Project;
069import org.ametys.runtime.i18n.I18nizableText;
070import org.ametys.runtime.parameter.ParameterHelper;
071import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
072import org.ametys.web.repository.page.ModifiablePage;
073import org.ametys.web.repository.page.ModifiableZone;
074import org.ametys.web.repository.page.ModifiableZoneItem;
075import org.ametys.web.repository.page.Page;
076import org.ametys.web.repository.page.ZoneItem.ZoneType;
077
078import com.google.common.collect.ImmutableSet;
079
080/**
081 * Helper component for managing calendars
082 */
083public class CalendarWorkspaceModule extends AbstractWorkspaceModule
084{
085    /** The id of calendar module */
086    public static final String CALENDAR_MODULE_ID = CalendarWorkspaceModule.class.getName();
087    
088    /** Tag on the main page holding the calendar module */
089    private static final String __CALENDAR_MODULE_TAG = "WORKSPACES_MODULE_CALENDAR";
090    
091    /** Workspaces calendars node name */
092    private static final String __WORKSPACES_CALENDARS_NODE_NAME = "calendars";
093    
094    private static final String __CALENDAR_CACHE_REQUEST_ATTR = CalendarWorkspaceModule.class.getName() + "$calendarCache";
095    
096    /** Module i18n title key */
097    private static final String __MODULE_TITLE_KEY = "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_LABEL";
098
099    private WorkspaceCalendarDAO _calendarDAO;
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        super.service(manager);
105        _calendarDAO = (WorkspaceCalendarDAO) manager.lookup(WorkspaceCalendarDAO.ROLE);
106    }
107    
108    @Override
109    public String getId()
110    {
111        return CALENDAR_MODULE_ID;
112    }
113    
114    @Override
115    public I18nizableText getModuleTitle()
116    {
117        return new I18nizableText("plugin." + _pluginName, __MODULE_TITLE_KEY);
118    }
119    
120    @Override
121    protected String getModulePageName()
122    {
123        return "calendars";
124    }
125    
126    @Override
127    protected I18nizableText getModulePageTitle()
128    {
129        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_CALENDARS_TITLE");
130    }
131    
132    @Override
133    protected String getModuleTagName()
134    {
135        return __CALENDAR_MODULE_TAG;
136    }
137    
138    @Override
139    protected void initializeModulePage(ModifiablePage calendarPage)
140    {
141        ModifiableZone defaultZone = calendarPage.createZone("default");
142        
143        String serviceId = "org.ametys.plugins.workspaces.module.Calendar";
144        ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
145        defaultZoneItem.setType(ZoneType.SERVICE);
146        defaultZoneItem.setServiceId(serviceId);
147        
148        ModifiableCompositeMetadata serviceMetadata = defaultZoneItem.getServiceParameters();
149        serviceMetadata.setMetadata("xslt", _getDefaultXslt(serviceId));
150    }
151    
152    /**
153     * Get the calendars of a project
154     * @param project The project
155     * @return The list of calendar
156     */
157    public AmetysObjectIterable<Calendar> getCalendars(Project project)
158    {
159        ModifiableResourceCollection moduleRoot = getModuleRoot(project, false);
160        return moduleRoot != null ? moduleRoot.getChildren() : null;
161    }
162    
163    @Override
164    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
165    {
166        try
167        {
168            ExplorerNode projectRootNode = project.getExplorerRootNode();
169            
170            if (projectRootNode instanceof ModifiableResourceCollection)
171            {
172                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
173                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_CALENDARS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
174            }
175            else
176            {
177                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
178            }
179        }
180        catch (AmetysRepositoryException e)
181        {
182            throw new AmetysRepositoryException("Error getting the documents root node.", e);
183        }
184    }
185
186    /**
187     * Utility method to get or create an ametys object
188     * @param <A> A sub class of AmetysObject
189     * @param parent The parent object
190     * @param name The ametys object name
191     * @param type The ametys object type
192     * @param create True to create the object if it does not exist
193     * @return ametys object
194     * @throws AmetysRepositoryException if an repository error occurs
195     */
196    protected <A extends AmetysObject> A _getAmetysObject(ModifiableTraversableAmetysObject parent, String name, String type, boolean create) throws AmetysRepositoryException
197    {
198        A object = null;
199        
200        if (parent.hasChild(name))
201        {
202            object = parent.getChild(name);
203        }
204        else if (create)
205        {
206            object = parent.createChild(name, type);
207            parent.saveChanges();
208        }
209        
210        return object;
211    }
212    
213    /**
214     * Retrieves the set of general rights used in the calendar module for the current user
215     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
216     */
217    @Callable
218    public Map<String, Object> getModuleBaseRights()
219    {
220        Request request = ContextHelper.getRequest(_context);
221        String projectName = (String) request.getAttribute("projectName");
222        
223        Project project = _projectManager.getProject(projectName);
224        ModifiableResourceCollection calendarRoot = getModuleRoot(project, false);
225        
226        Map<String, Object> rightsData = new HashMap<>();
227        UserIdentity user = _currentUserProvider.getUser();
228        
229        // Add calendar
230        rightsData.put("add-calendar", calendarRoot != null && _rightManager.hasRight(user, CalendarDAO.RIGHTS_CALENDAR_ADD, calendarRoot) == RightResult.RIGHT_ALLOW);
231        
232        // Tags
233        rightsData.put("add-tag", _projectRightHelper.hasRight(WorkspaceCalendarDAO.RIGHTS_TAG_ADD, project));
234        rightsData.put("remove-tag", _projectRightHelper.hasRight(WorkspaceCalendarDAO.RIGHTS_TAG_DELETE, project));
235        
236        // Places
237        rightsData.put("add-place", _projectRightHelper.hasRight(WorkspaceCalendarDAO.RIGHTS_PLACE_ADD, project));
238        rightsData.put("remove-place", _projectRightHelper.hasRight(WorkspaceCalendarDAO.RIGHTS_PLACE_DELETE, project));
239        
240        return rightsData;
241    }
242    
243    /**
244     * Get the rights of the current user for the calendar service
245     * @param calendarIds The list of calendars
246     * @return The rights
247     */
248    @Callable
249    public Map<String, Object> getCalendarServiceRights(List<String> calendarIds)
250    {
251        Map<String, Object> rights = new HashMap<>();
252        
253        List<String> rightEventAdd = new ArrayList<>();
254        List<String> rightTagAdd = new ArrayList<>();
255        List<String> rightPlaceAdd = new ArrayList<>();
256        
257        UserIdentity currentUser = _currentUserProvider.getUser();
258        
259        List<String> ids = calendarIds;
260        if (calendarIds == null || (calendarIds.size() == 1 && calendarIds.get(0) == null))
261        {
262            ids = getCalendarsData().stream().map(c -> (String) c.get("id")).collect(Collectors.toList());
263        }
264        
265        for (String calendarId : ids)
266        {
267            Calendar calendar = _resolver.resolveById(calendarId);
268            
269            if (_rightManager.hasRight(currentUser, CalendarDAO.RIGHTS_EVENT_ADD, calendar) == RightResult.RIGHT_ALLOW)
270            {
271                rightEventAdd.add(calendarId);
272            }
273        }
274        
275        rights.put("eventAdd", rightEventAdd);
276        rights.put("tagAdd", rightTagAdd);
277        rights.put("placetAdd", rightPlaceAdd);
278        
279        return rights;
280    }
281    
282    /**
283     * Get the URI of a thread in project'site
284     * @param project The project
285     * @param calendarId The id of calendar
286     * @param eventId The id of event
287     * @param language The sitemap language
288     * @return The thread uri
289     */
290    public String getEventUri(Project project, String calendarId, String eventId, String language)
291    {
292        AmetysObjectIterable<Page> pages = getModulePages(project, language);
293        
294        if (pages.getSize() > 0)
295        {
296            Page page = pages.iterator().next();
297            
298            StringBuilder sb = new StringBuilder();
299            sb.append(ResolveURIComponent.resolve("page", page.getId()));
300            
301            try
302            {
303                CalendarEvent event = _resolver.resolveById(eventId);
304                
305                if (event.getStartDate() != null)
306                {
307                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
308                    sb.append("?date=").append(df.format(event.getStartDate()));
309                }
310            }
311            catch (UnknownAmetysObjectException e)
312            {
313                // Nothing
314            }
315            
316            sb.append("#").append(calendarId);
317            
318            return sb.toString();
319        }
320        
321        return null;
322    }
323    
324    /**
325     * Add a calendar
326     * @param inputName The desired name for the calendar
327     * @param description The calendar description
328     * @param templateDesc The calendar template description
329     * @param color The calendar color
330     * @param visibility The calendar visibility
331     * @param workflowName The calendar workflow name
332     * @param renameIfExists True to rename if existing
333     * @return The result map with id, parentId and name keys
334     * @throws IllegalAccessException If the user has no sufficient rights
335     */
336    @Callable
337    public Map<String, Object> addCalendar(String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists) throws IllegalAccessException
338    {
339        Request request = ContextHelper.getRequest(_context);
340        String projectName = (String) request.getAttribute("projectName");
341        
342        Project project = _projectManager.getProject(projectName);
343        ModifiableResourceCollection calendarRoot = getModuleRoot(project, false);
344        assert calendarRoot != null;
345        
346        // TODO catch IllegalAccessException -> error = has-right
347        
348        return _calendarDAO.addCalendar(calendarRoot.getId(), inputName, description, templateDesc, color, visibility, workflowName, renameIfExists);
349    }
350    
351    /**
352     * Add additional information on project and parent calendar
353     * @param event The event
354     * @param eventData The event data to complete
355     */
356    @SuppressWarnings("unchecked")
357    protected void _addAdditionalEventData(CalendarEvent event, Map<String, Object> eventData)
358    {
359        Request request = ContextHelper.getRequest(_context);
360        String language = (String) request.getAttribute("sitemapLanguage");
361        
362        Calendar calendar = event.getParent();
363        Project project = _projectManager.getParentProject(calendar);
364        
365        // Try to get calendar from cache if request is not null
366        if (request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR) == null)
367        {
368            request.setAttribute(__CALENDAR_CACHE_REQUEST_ATTR, new HashMap<String, Object>());
369        }
370        
371        Map<String, Object> calendarCache = (Map<String, Object>) request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR);
372        
373        if (!calendarCache.containsKey(calendar.getId()))
374        {
375            Map<String, Object> calendarInfo = new HashMap<>();
376            
377            calendarInfo.put("calendarName", calendar.getName());
378            calendarInfo.put("calendarIsPublic", CalendarVisibility.PUBLIC.equals(calendar.getVisibility()));
379            calendarInfo.put("calendarHasViewRight", canView(calendar));
380            
381            calendarInfo.put("projectId", project.getId());
382            calendarInfo.put("projectTitle", project.getTitle());
383            
384            AmetysObjectIterable<Page> calendarModulePages = getModulePages(project, language);
385            if (calendarModulePages.getSize() > 0)
386            {
387                Page calendarModulePage = calendarModulePages.iterator().next();
388                calendarInfo.put("calendarModulePageId", calendarModulePage.getId());
389            }
390            
391            calendarCache.put(calendar.getId(), calendarInfo);
392        }
393        
394        eventData.putAll((Map<String, Object>) calendarCache.get(calendar.getId()));
395       
396        eventData.put("eventUrl", getEventUri(project, calendar.getId(), event.getId(), language));
397    }
398    
399    
400    
401    /**
402     * Get the upcoming events of the calendars on which the user has a right
403     * @param months the amount of months from today in which look for upcoming events
404     * @param maxResults the maximum results to display
405     * @param calendarIds the ids of the calendars to gather events from, null for all calendars
406     * @param tagIds the ids of the valid tags for the events, null for any tag 
407     * @return the upcoming events
408     */
409    @Callable
410    public List<Map<String, Object>> getUpcomingEvents(String months, String maxResults, List<String> calendarIds, List<String> tagIds)
411    {
412        int monthsAsInt = StringUtils.isBlank(months) ? 3 : Integer.parseInt(months);
413        int maxResultsAsInt = StringUtils.isBlank(maxResults) ? Integer.MAX_VALUE : Integer.parseInt(maxResults);
414        
415        return getUpcomingEvents(monthsAsInt, maxResultsAsInt, calendarIds, tagIds);
416    }
417    
418    /**
419     * Get the upcoming events of the calendars on which the user has a right
420     * @param months the amount of months from today in which look for upcoming events
421     * @param maxResults the maximum results to display
422     * @param calendarIds the ids of the calendars to gather events from, null for all calendars
423     * @param tagIds the ids of the valid tags for the events, null for any tag 
424     * @return the upcoming events
425     */
426    public List<Map<String, Object>> getUpcomingEvents(int months, int maxResults, List<String> calendarIds, List<String> tagIds)
427    {
428        List<Map<String, Object>> basicEventList = new ArrayList<> ();
429        List<Map<String, Object>> recurrentEventList = new ArrayList<> ();
430        
431        java.util.Calendar cal = java.util.Calendar.getInstance();
432        cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
433        cal.set(java.util.Calendar.MINUTE, 0);
434        cal.set(java.util.Calendar.SECOND, 0);
435        cal.set(java.util.Calendar.MILLISECOND, 0);
436        Date startDate = cal.getTime();
437
438        Date endDate = DateUtils.addMonths(startDate, months);
439        
440        Expression nonRecurrentExpr = new StringExpression(JCRCalendarEvent.METADATA_RECURRENCE_TYPE, Operator.EQ, "NEVER");
441        Expression startDateExpr = new DateExpression(JCRCalendarEvent.METADATA_START_DATE, Operator.GE, startDate);
442        Expression endDateExpr = new DateExpression(JCRCalendarEvent.METADATA_START_DATE, Operator.LE, endDate);
443        
444        Expression keywordsExpr = null;
445        
446        if (tagIds != null && !tagIds.isEmpty())
447        {
448            List<Expression> orExpr = new ArrayList<>();
449            for (String tagId : tagIds)
450            {
451                orExpr.add(new StringExpression(JCRCalendarEvent.METADATA_KEYWORDS, Operator.EQ, tagId));
452            }
453            keywordsExpr = new OrExpression(orExpr.toArray(new Expression[orExpr.size()]));
454        }
455        
456        // Get the non recurrent events sorted by ascending date and within the configured range
457        Expression eventExpr = new AndExpression(nonRecurrentExpr, startDateExpr, endDateExpr, keywordsExpr);
458        SortCriteria sortCriteria = new SortCriteria();
459        sortCriteria.addCriterion(JCRCalendarEvent.METADATA_START_DATE, true, false);
460        
461        String basicEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
462        AmetysObjectIterable<CalendarEvent> basicEvents = _resolver.query(basicEventQuery);
463        AmetysObjectIterator<CalendarEvent> basicEventIt = basicEvents.iterator();
464        
465        int processed = 0;
466        while (basicEventIt.hasNext() && processed < maxResults)
467        {
468            CalendarEvent event = basicEventIt.next();
469            Calendar holdingCalendar = (Calendar) event.getParent();
470            
471            if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
472            {
473                // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
474                
475                // FIXME should use something like an EventInfo object with some data + calendar, project name
476                // And use a function to process the transformation...
477                // Function<EventInfo, Map<String, Object>> fn. eventData.putAll(fn.apply(info));
478                
479                // standard set of event data
480                Map<String, Object> eventData = _calendarDAO.getEventData(event, false);
481                basicEventList.add(eventData);
482                processed++;
483                
484                // add additional info
485                _addAdditionalEventData(event, eventData);
486            }
487        }
488        
489        Expression recurrentExpr = new StringExpression(JCRCalendarEvent.METADATA_RECURRENCE_TYPE, Operator.NE, "NEVER");
490        eventExpr = new AndExpression(recurrentExpr, keywordsExpr);
491        
492        String recurrentEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
493        AmetysObjectIterable<CalendarEvent> recurrentEvents = _resolver.query(recurrentEventQuery);
494        AmetysObjectIterator<CalendarEvent> recurrentEventIt = recurrentEvents.iterator();
495        
496        Date nextDate = null;
497        // FIXME cannot count processed here...
498        processed = 0;
499        while (recurrentEventIt.hasNext() /*&& processed < maxResultsAsInt*/)
500        {
501            CalendarEvent event = recurrentEventIt.next();
502            nextDate = event.getNextOccurrence(startDate);
503            
504            // The recurrent event first occurrence is within the range
505            if (nextDate.before(endDate))
506            {
507                // FIXME calculate occurrences only if keep event...
508                List<Date> occurrences = event.getOccurrences(startDate, endDate);
509                Calendar holdingCalendar = (Calendar) event.getParent();
510                
511                if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
512                {
513                    // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
514                    
515                    // Add all its occurrences that are within the range
516                    for (Date occurrence : occurrences)
517                    {
518                        Map<String, Object> eventData = _calendarDAO.getEventData(event, occurrence, false);
519                        recurrentEventList.add(eventData);
520                        processed++;
521                        
522                        _addAdditionalEventData(event, eventData);
523                        
524                    }
525                }
526            }
527        }
528        
529        // Re-sort chronologically the events' union
530        List<Map<String, Object>> allEvents = ListUtils.union(basicEventList, recurrentEventList);
531        Collections.sort(allEvents, new StartDateComparator());
532
533        // Return the first maxResults events
534        return allEvents.size() <= maxResults ? allEvents : allEvents.subList(0, maxResults);
535    }
536    
537    /**
538     * Determine whether the given event has to be kept or not depending on the given calendars
539     * @param calendarIds the ids of the calendars
540     * @param event the event
541     * @return true if the event can be kept, false otherwise
542     */
543    private boolean _filterEvent(List<String> calendarIds, CalendarEvent event)
544    {
545        Calendar holdingCalendar = (Calendar) event.getParent();
546        // FIXME calendarIds.get(0) == null means "All calendars" selected in the select calendar widget ??
547        // need cleaner code
548        return calendarIds == null || calendarIds.get(0) == null || calendarIds.contains(holdingCalendar.getId());
549    }
550    
551    private boolean _hasAccess(Calendar calendar)
552    {
553        return CalendarVisibility.PUBLIC.equals(calendar.getVisibility()) || canView(calendar);
554    }
555    
556    /**
557     * Get the data of every available calendar of the application
558     * @return the list of calendar data
559     */
560    @Callable
561    public List<Map<String, Object>> getCalendarsData()
562    {
563        List<Map<String, Object>> calendarsData = new ArrayList<>();
564        
565        AmetysObjectIterable<Project> projects = _projectManager.getProjects();
566        for (Project project : projects)
567        {
568            AmetysObjectIterable<Calendar> calendars = getCalendars(project);
569            if (calendars != null)
570            {
571                for (Calendar calendar : calendars)
572                {
573                    if (canView(calendar))
574                    {
575                        calendarsData.add(_calendarDAO.getCalendarData(calendar, false, false));
576                    }
577                }
578            }
579        }
580        
581        return calendarsData;
582    }
583    
584    /**
585     * Indicates if the current user can view the calendar
586     * @param calendar The calendar to test
587     * @return true if the calendar can be viewed 
588     */
589    public boolean canView(Calendar calendar)
590    {
591        return _projectRightHelper.hasReadAccess(calendar);
592    }
593    
594    /**
595     * Indicates if the current user can view the event
596     * @param event The event to test
597     * @return true if the event can be viewed 
598     */
599    public boolean canView(CalendarEvent event)
600    {
601        return _projectRightHelper.hasReadAccess(event.<ExplorerNode>getParent());
602    }
603    
604    
605    /**
606     * Compares events on their starting date
607     */
608    protected static class StartDateComparator implements Comparator<Map<String, Object>>
609    {
610        @Override
611        public int compare(Map<String, Object> calendarEventInfo1, Map<String, Object> calendarEventInfo2)
612        {
613            String startDate1asString = (String) calendarEventInfo1.get("startDate");
614            String startDate2asString = (String) calendarEventInfo2.get("startDate");
615            
616            Date startDate1 = (Date) ParameterHelper.castValue(startDate1asString, ParameterType.DATE);
617            Date startDate2 = (Date) ParameterHelper.castValue(startDate2asString, ParameterType.DATE);
618            
619            // The start date is before if 
620            return startDate1.compareTo(startDate2);
621        }
622    }
623    
624    @Override
625    public Set<String> getAllowedEventTypes()
626    {
627        return ImmutableSet.of("calendar.event.created", "calendar.event.updated");
628    }
629
630}
631