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