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