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