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