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.time.ZonedDateTime; 021import java.time.temporal.ChronoUnit; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Optional; 031import java.util.Set; 032import java.util.stream.Stream; 033 034import org.apache.avalon.framework.configuration.Configurable; 035import org.apache.avalon.framework.configuration.Configuration; 036import org.apache.avalon.framework.configuration.ConfigurationException; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.cocoon.components.ContextHelper; 040import org.apache.cocoon.environment.Request; 041import org.apache.commons.collections.ListUtils; 042 043import org.ametys.core.util.DateUtils; 044import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 045import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 046import org.ametys.plugins.repository.AmetysObjectIterable; 047import org.ametys.plugins.repository.AmetysObjectIterator; 048import org.ametys.plugins.repository.TraversableAmetysObject; 049import org.ametys.plugins.repository.UnknownAmetysObjectException; 050import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 051import org.ametys.plugins.repository.query.QueryHelper; 052import org.ametys.plugins.repository.query.SortCriteria; 053import org.ametys.plugins.repository.query.expression.AndExpression; 054import org.ametys.plugins.repository.query.expression.DateExpression; 055import org.ametys.plugins.repository.query.expression.Expression; 056import org.ametys.plugins.repository.query.expression.Expression.Operator; 057import org.ametys.plugins.repository.query.expression.OrExpression; 058import org.ametys.plugins.repository.query.expression.StringExpression; 059import org.ametys.plugins.workspaces.AbstractWorkspaceModule; 060import org.ametys.plugins.workspaces.calendars.Calendar.CalendarVisibility; 061import org.ametys.plugins.workspaces.calendars.events.CalendarEvent; 062import org.ametys.plugins.workspaces.calendars.events.CalendarEventJSONHelper; 063import org.ametys.plugins.workspaces.calendars.events.CalendarEventOccurrence; 064import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent; 065import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEventFactory; 066import org.ametys.plugins.workspaces.project.objects.Project; 067import org.ametys.plugins.workspaces.util.StatisticColumn; 068import org.ametys.plugins.workspaces.util.StatisticsColumnType; 069import org.ametys.runtime.i18n.I18nizableText; 070import org.ametys.web.repository.page.ModifiablePage; 071import org.ametys.web.repository.page.ModifiableZone; 072import org.ametys.web.repository.page.ModifiableZoneItem; 073import org.ametys.web.repository.page.Page; 074import org.ametys.web.repository.page.ZoneItem.ZoneType; 075import org.ametys.web.repository.site.Site; 076import org.ametys.web.repository.sitemap.Sitemap; 077 078import com.google.common.collect.ImmutableSet; 079 080/** 081 * Helper component for managing calendars 082 */ 083public class CalendarWorkspaceModule extends AbstractWorkspaceModule implements Configurable 084{ 085 /** The id of calendar module */ 086 public static final String CALENDAR_MODULE_ID = CalendarWorkspaceModule.class.getName(); 087 088 /** Workspaces calendars node name */ 089 private static final String __WORKSPACES_CALENDARS_NODE_NAME = "calendars"; 090 091 /** Workspaces root tasks node name */ 092 private static final String __WORKSPACES_CALENDARS_ROOT_NODE_NAME = "calendars-root"; 093 094 /** Workspaces root tasks node name */ 095 private static final String __WORKSPACES_CALENDAR_RESOURCES_ROOT_NODE_NAME = "calendar-resources-root"; 096 097 /** Workspaces root tasks node name */ 098 private static final String __WORKSPACES_RESOURCE_CALENDAR_ROOT_NODE_NAME = "resource-calendar-root"; 099 100 private static final String __CALENDAR_CACHE_REQUEST_ATTR = CalendarWorkspaceModule.class.getName() + "$calendarCache"; 101 102 private static final String __EVENT_NUMBER_HEADER_ID = __WORKSPACES_CALENDARS_NODE_NAME + "$event_number"; 103 104 private CalendarDAO _calendarDAO; 105 private CalendarEventJSONHelper _calendarEventJSONHelper; 106 107 private I18nizableText _defaultCalendarTemplateDesc; 108 private String _defaultCalendarColor; 109 private String _defaultCalendarVisibility; 110 private String _defaultCalendarWorkflowName; 111 private I18nizableText _defaultCalendarTitle; 112 private I18nizableText _defaultCalendarDescription; 113 114 private I18nizableText _resourceCalendarTemplateDesc; 115 private String _resourceCalendarColor; 116 private String _resourceCalendarVisibility; 117 private String _resourceCalendarWorkflowName; 118 private I18nizableText _resourceCalendarTitle; 119 private I18nizableText _resourceCalendarDescription; 120 121 @Override 122 public void service(ServiceManager manager) throws ServiceException 123 { 124 super.service(manager); 125 _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE); 126 _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE); 127 } 128 129 public void configure(Configuration configuration) throws ConfigurationException 130 { 131 _defaultCalendarTemplateDesc = I18nizableText.parseI18nizableText(configuration.getChild("template-desc"), "plugin." + _pluginName, ""); 132 _defaultCalendarColor = configuration.getChild("color").getValue("col1"); 133 _defaultCalendarVisibility = configuration.getChild("visibility").getValue(CalendarVisibility.PRIVATE.name()); 134 _defaultCalendarWorkflowName = configuration.getChild("workflow").getValue("calendar-default"); 135 _defaultCalendarTitle = I18nizableText.parseI18nizableText(configuration.getChild("title"), "plugin." + _pluginName); 136 _defaultCalendarDescription = I18nizableText.parseI18nizableText(configuration.getChild("description"), "plugin." + _pluginName, ""); 137 138 _resourceCalendarTemplateDesc = I18nizableText.parseI18nizableText(configuration.getChild("resource-template-desc"), "plugin." + _pluginName, ""); 139 _resourceCalendarColor = configuration.getChild("resource-color").getValue("resourcecol0"); 140 _resourceCalendarVisibility = configuration.getChild("resource-visibility").getValue(CalendarVisibility.PRIVATE.name()); 141 _resourceCalendarWorkflowName = configuration.getChild("resource-workflow").getValue("calendar-default"); 142 _resourceCalendarTitle = I18nizableText.parseI18nizableText(configuration.getChild("resource-title"), "plugin." + _pluginName); 143 _resourceCalendarDescription = I18nizableText.parseI18nizableText(configuration.getChild("resource-description"), "plugin." + _pluginName, ""); 144 145 } 146 147 @Override 148 public String getId() 149 { 150 return CALENDAR_MODULE_ID; 151 } 152 153 public int getOrder() 154 { 155 return ORDER_CALENDAR; 156 } 157 158 public String getModuleName() 159 { 160 return __WORKSPACES_CALENDARS_NODE_NAME; 161 } 162 163 @Override 164 protected String getModulePageName() 165 { 166 return "calendars"; 167 } 168 169 public I18nizableText getModuleTitle() 170 { 171 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_LABEL"); 172 } 173 public I18nizableText getModuleDescription() 174 { 175 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_DESCRIPTION"); 176 } 177 @Override 178 protected I18nizableText getModulePageTitle() 179 { 180 return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_CALENDARS_TITLE"); 181 } 182 183 @Override 184 protected void initializeModulePage(ModifiablePage calendarPage) 185 { 186 ModifiableZone defaultZone = calendarPage.createZone("default"); 187 188 String serviceId = "org.ametys.plugins.workspaces.module.Calendar"; 189 ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem(); 190 defaultZoneItem.setType(ZoneType.SERVICE); 191 defaultZoneItem.setServiceId(serviceId); 192 193 ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters(); 194 serviceDataHolder.setValue("xslt", _getDefaultXslt(serviceId)); 195 } 196 197 /** 198 * Get the calendars of a project 199 * @param project The project 200 * @return The list of calendar 201 */ 202 public AmetysObjectIterable<Calendar> getCalendars(Project project) 203 { 204 ModifiableResourceCollection calendarRoot = getCalendarsRoot(project, true); 205 return calendarRoot != null ? calendarRoot.getChildren() : null; 206 } 207 208 /** 209 * Get the URI of a thread in project'site 210 * @param project The project 211 * @param calendarId The id of calendar 212 * @param eventId The id of event 213 * @return The thread uri 214 */ 215 public String getEventUri(Project project, String calendarId, String eventId) 216 { 217 String moduleUrl = getModuleUrl(project); 218 if (moduleUrl != null) 219 { 220 StringBuilder sb = new StringBuilder(); 221 sb.append(moduleUrl); 222 223 try 224 { 225 CalendarEvent event = _resolver.resolveById(eventId); 226 227 if (event.getStartDate() != null) 228 { 229 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 230 sb.append("?date=").append(df.format(DateUtils.asDate(event.getStartDate()))); 231 } 232 } 233 catch (UnknownAmetysObjectException e) 234 { 235 // Nothing 236 } 237 238 sb.append("#").append(calendarId); 239 240 return sb.toString(); 241 } 242 243 return null; 244 } 245 246 /** 247 * Add additional information on project and parent calendar 248 * @param event The event 249 * @param eventData The event data to complete 250 */ 251 @SuppressWarnings("unchecked") 252 protected void _addAdditionalEventData(CalendarEvent event, Map<String, Object> eventData) 253 { 254 Request request = ContextHelper.getRequest(_context); 255 256 Calendar calendar = event.getParent(); 257 Project project = _projectManager.getParentProject(calendar); 258 259 // Try to get calendar from cache if request is not null 260 if (request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR) == null) 261 { 262 request.setAttribute(__CALENDAR_CACHE_REQUEST_ATTR, new HashMap<>()); 263 } 264 265 Map<String, Object> calendarCache = (Map<String, Object>) request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR); 266 267 if (!calendarCache.containsKey(calendar.getId())) 268 { 269 Map<String, Object> calendarInfo = new HashMap<>(); 270 271 calendarInfo.put("calendarName", calendar.getName()); 272 calendarInfo.put("calendarIsPublic", CalendarVisibility.PUBLIC.equals(calendar.getVisibility())); 273 calendarInfo.put("calendarHasViewRight", canView(calendar)); 274 275 calendarInfo.put("projectId", project.getId()); 276 calendarInfo.put("projectTitle", project.getTitle()); 277 278 Set<Page> calendarModulePages = _projectManager.getModulePages(project, this); 279 if (!calendarModulePages.isEmpty()) 280 { 281 Page calendarModulePage = calendarModulePages.iterator().next(); 282 calendarInfo.put("calendarModulePageId", calendarModulePage.getId()); 283 } 284 285 calendarCache.put(calendar.getId(), calendarInfo); 286 } 287 288 eventData.putAll((Map<String, Object>) calendarCache.get(calendar.getId())); 289 290 eventData.put("eventUrl", getEventUri(project, calendar.getId(), event.getId())); 291 } 292 293 /** 294 * Get the upcoming events of the calendars on which the user has a right 295 * @param months the amount of months from today in which look for upcoming events 296 * @param maxResults the maximum results to display 297 * @param calendarIds the ids of the calendars to gather events from, null for all calendars 298 * @param tagIds the ids of the valid tags for the events, null for any tag 299 * @return the upcoming events 300 */ 301 public List<Map<String, Object>> getUpcomingEvents(int months, int maxResults, List<String> calendarIds, List<String> tagIds) 302 { 303 List<Map<String, Object>> basicEventList = new ArrayList<> (); 304 List<Map<String, Object>> recurrentEventList = new ArrayList<> (); 305 306 java.util.Calendar cal = java.util.Calendar.getInstance(); 307 cal.set(java.util.Calendar.HOUR_OF_DAY, 0); 308 cal.set(java.util.Calendar.MINUTE, 0); 309 cal.set(java.util.Calendar.SECOND, 0); 310 cal.set(java.util.Calendar.MILLISECOND, 0); 311 ZonedDateTime startDate = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS); 312 313 ZonedDateTime endDate = startDate.plusMonths(months); 314 315 Expression nonRecurrentExpr = new StringExpression(JCRCalendarEvent.ATTRIBUTE_RECURRENCE_TYPE, Operator.EQ, "NEVER"); 316 Expression startDateExpr = new DateExpression(JCRCalendarEvent.ATTRIBUTE_START_DATE, Operator.GE, DateUtils.asDate(startDate)); 317 Expression endDateExpr = new DateExpression(JCRCalendarEvent.ATTRIBUTE_START_DATE, Operator.LE, DateUtils.asDate(endDate)); 318 319 Expression keywordsExpr = null; 320 321 if (tagIds != null && !tagIds.isEmpty()) 322 { 323 List<Expression> orExpr = new ArrayList<>(); 324 for (String tagId : tagIds) 325 { 326 orExpr.add(new StringExpression(JCRCalendarEvent.ATTRIBUTE_KEYWORDS, Operator.EQ, tagId)); 327 } 328 keywordsExpr = new OrExpression(orExpr.toArray(new Expression[orExpr.size()])); 329 } 330 331 // Get the non recurrent events sorted by ascending date and within the configured range 332 Expression eventExpr = new AndExpression(nonRecurrentExpr, startDateExpr, endDateExpr, keywordsExpr); 333 SortCriteria sortCriteria = new SortCriteria(); 334 sortCriteria.addCriterion(JCRCalendarEvent.ATTRIBUTE_START_DATE, true, false); 335 336 String basicEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria); 337 AmetysObjectIterable<CalendarEvent> basicEvents = _resolver.query(basicEventQuery); 338 AmetysObjectIterator<CalendarEvent> basicEventIt = basicEvents.iterator(); 339 340 int processed = 0; 341 while (basicEventIt.hasNext() && processed < maxResults) 342 { 343 CalendarEvent event = basicEventIt.next(); 344 Calendar holdingCalendar = (Calendar) event.getParent(); 345 346 if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar)) 347 { 348 // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null) 349 350 // FIXME should use something like an EventInfo object with some data + calendar, project name 351 // And use a function to process the transformation... 352 // Function<EventInfo, Map<String, Object>> fn. eventData.putAll(fn.apply(info)); 353 354 // standard set of event data 355 Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false, false); 356 basicEventList.add(eventData); 357 processed++; 358 359 // add additional info 360 _addAdditionalEventData(event, eventData); 361 } 362 } 363 364 Expression recurrentExpr = new StringExpression(JCRCalendarEvent.ATTRIBUTE_RECURRENCE_TYPE, Operator.NE, "NEVER"); 365 eventExpr = new AndExpression(recurrentExpr, keywordsExpr); 366 367 String recurrentEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria); 368 AmetysObjectIterable<CalendarEvent> recurrentEvents = _resolver.query(recurrentEventQuery); 369 AmetysObjectIterator<CalendarEvent> recurrentEventIt = recurrentEvents.iterator(); 370 371 // FIXME cannot count processed here... 372 processed = 0; 373 while (recurrentEventIt.hasNext() /*&& processed < maxResultsAsInt*/) 374 { 375 CalendarEvent event = recurrentEventIt.next(); 376 Optional<CalendarEventOccurrence> nextOccurrence = event.getNextOccurrence(new CalendarEventOccurrence(event, startDate)); 377 378 // The recurrent event first occurrence is within the range 379 if (nextOccurrence.isPresent() && nextOccurrence.get().before(endDate)) 380 { 381 // FIXME calculate occurrences only if keep event... 382 List<CalendarEventOccurrence> occurrences = event.getOccurrences(nextOccurrence.get().getStartDate(), endDate); 383 Calendar holdingCalendar = (Calendar) event.getParent(); 384 385 if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar)) 386 { 387 // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null) 388 389 // Add all its occurrences that are within the range 390 for (CalendarEventOccurrence occurrence : occurrences) 391 { 392 Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJsonWithOccurrence(event, occurrence.getStartDate(), false); 393 recurrentEventList.add(eventData); 394 processed++; 395 396 _addAdditionalEventData(event, eventData); 397 398 } 399 } 400 } 401 } 402 403 // Re-sort chronologically the events' union 404 List<Map<String, Object>> allEvents = ListUtils.union(basicEventList, recurrentEventList); 405 Collections.sort(allEvents, new StartDateComparator()); 406 407 // Return the first maxResults events 408 return allEvents.size() <= maxResults ? allEvents : allEvents.subList(0, maxResults); 409 } 410 411 /** 412 * Determine whether the given event has to be kept or not depending on the given calendars 413 * @param calendarIds the ids of the calendars 414 * @param event the event 415 * @return true if the event can be kept, false otherwise 416 */ 417 private boolean _filterEvent(List<String> calendarIds, CalendarEvent event) 418 { 419 Calendar holdingCalendar = (Calendar) event.getParent(); 420 // FIXME calendarIds.get(0) == null means "All calendars" selected in the select calendar widget ?? 421 // need cleaner code 422 return calendarIds == null || calendarIds.get(0) == null || calendarIds.contains(holdingCalendar.getId()); 423 } 424 425 private boolean _hasAccess(Calendar calendar) 426 { 427 return CalendarVisibility.PUBLIC.equals(calendar.getVisibility()) || canView(calendar); 428 } 429 430 /** 431 * Indicates if the current user can view the calendar 432 * @param calendar The calendar to test 433 * @return true if the calendar can be viewed 434 */ 435 public boolean canView(Calendar calendar) 436 { 437 return _rightManager.currentUserHasReadAccess(calendar); 438 } 439 440 /** 441 * Indicates if the current user can view the event 442 * @param event The event to test 443 * @return true if the event can be viewed 444 */ 445 public boolean canView(CalendarEvent event) 446 { 447 return _rightManager.currentUserHasReadAccess(event.getParent()); 448 } 449 450 /** 451 * Compares events on their starting date 452 */ 453 protected static class StartDateComparator implements Comparator<Map<String, Object>> 454 { 455 @Override 456 public int compare(Map<String, Object> calendarEventInfo1, Map<String, Object> calendarEventInfo2) 457 { 458 String startDate1asString = (String) calendarEventInfo1.get("startDate"); 459 String startDate2asString = (String) calendarEventInfo2.get("startDate"); 460 461 Date startDate1 = DateUtils.parse(startDate1asString); 462 Date startDate2 = DateUtils.parse(startDate2asString); 463 464 // The start date is before if 465 return startDate1.compareTo(startDate2); 466 } 467 } 468 469 @Override 470 public Set<String> getAllowedEventTypes() 471 { 472 return ImmutableSet.of("calendar.event.created", "calendar.event.updated"); 473 } 474 475 @Override 476 protected void _internalActivateModule(Project project, Map<String, Object> additionalValues) 477 { 478 createResourceCalendar(project, additionalValues); 479 _createDefaultCalendar(project, additionalValues); 480 } 481 482 /** 483 * Create a calendar to store resources if needed 484 * @param project the project 485 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 486 * @return The resource calendar 487 */ 488 public Calendar createResourceCalendar(Project project, Map<String, Object> additionalValues) 489 { 490 ModifiableResourceCollection resourceCalendarRoot = getResourceCalendarRoot(project, true); 491 492 String lang; 493 if (additionalValues.containsKey("language")) 494 { 495 lang = (String) additionalValues.get("language"); 496 } 497 else 498 { 499 lang = getLang(project); 500 } 501 502 Calendar resourceCalendar = resourceCalendarRoot.getChildren() 503 .stream() 504 .filter(Calendar.class::isInstance) 505 .map(Calendar.class::cast) 506 .findFirst() 507 .orElse(null); 508 509 if (resourceCalendar == null) 510 { 511 Boolean renameIfExists = false; 512 Boolean checkRights = false; 513 String description = _i18nUtils.translate(_resourceCalendarDescription, lang); 514 String inputName = _i18nUtils.translate(_resourceCalendarTitle, lang); 515 String templateDesc = _i18nUtils.translate(_resourceCalendarTemplateDesc, lang); 516 try 517 { 518 Map result = _calendarDAO.addCalendar(resourceCalendarRoot, inputName, description, templateDesc, _resourceCalendarColor, _resourceCalendarVisibility, _resourceCalendarWorkflowName, renameIfExists, checkRights, false); 519 520 resourceCalendar = _resolver.resolveById((String) result.get("id")); 521 } 522 catch (Exception e) 523 { 524 getLogger().error("Error while trying to create the first calendar in a newly created project", e); 525 } 526 } 527 return resourceCalendar; 528 } 529 530 private void _createDefaultCalendar(Project project, Map<String, Object> additionalValues) 531 { 532 ModifiableResourceCollection moduleRoot = getCalendarsRoot(project, true); 533 534 if (moduleRoot != null && !_hasOtherCalendar(project)) 535 { 536 Boolean renameIfExists = false; 537 Boolean checkRights = false; 538 539 String lang; 540 if (additionalValues.containsKey("language")) 541 { 542 lang = (String) additionalValues.get("language"); 543 } 544 else 545 { 546 lang = getLang(project); 547 } 548 549 String description = _i18nUtils.translate(_defaultCalendarDescription, lang); 550 String inputName = _i18nUtils.translate(_defaultCalendarTitle, lang); 551 String templateDesc = _i18nUtils.translate(_defaultCalendarTemplateDesc, lang); 552 try 553 { 554 _calendarDAO.addCalendar(moduleRoot, inputName, description, templateDesc, _defaultCalendarColor, _defaultCalendarVisibility, _defaultCalendarWorkflowName, renameIfExists, checkRights, false); 555 } 556 catch (Exception e) 557 { 558 getLogger().error("Error while trying to create the first calendar in a newly created project", e); 559 } 560 561 } 562 } 563 564 private boolean _hasOtherCalendar(Project project) 565 { 566 AmetysObjectIterable<Calendar> calendars = getCalendars(project); 567 568 return calendars != null && calendars.getSize() > 0; 569 } 570 571 /** 572 * Get the lang for this project, or fr if none found 573 * @param project the project to check 574 * @return a lang in this project 575 */ 576 protected String getLang(Project project) 577 { 578 /* 579 * get the site, then the sitemaps (transformed back to a stream of sitemap) 580 * then get the name 581 * return the 1st one, or "en" by default if none is available, or if the site could not be found 582 */ 583 584 Site site = project.getSite(); 585 586 if (site != null) 587 { 588 return site.getSitemaps() 589 .stream() 590 .filter(Objects::nonNull) 591 .map(Sitemap::getName) 592 .findFirst() 593 .orElse("en"); 594 } 595 else 596 { 597 return "en"; 598 } 599 } 600 601 /** 602 * Get the calendars of a project 603 * @param project The project 604 * @return The list of calendar 605 */ 606 public Calendar getResourceCalendar(Project project) 607 { 608 ModifiableResourceCollection resourceCalendarRoot = getResourceCalendarRoot(project, true); 609 return resourceCalendarRoot.getChildren() 610 .stream() 611 .filter(Calendar.class::isInstance) 612 .map(Calendar.class::cast) 613 .findFirst() 614 .orElse(createResourceCalendar(project, new HashMap<>())); 615 } 616 617 /** 618 * Get the root for tasks 619 * @param project The project 620 * @param create true to create root if not exists 621 * @return The root for tasks 622 */ 623 public ModifiableResourceCollection getCalendarResourcesRoot(Project project, boolean create) 624 { 625 ModifiableResourceCollection moduleRoot = getModuleRoot(project, create); 626 return _getAmetysObject(moduleRoot, __WORKSPACES_CALENDAR_RESOURCES_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 627 } 628 629 /** 630 * Get the root for tasks 631 * @param project The project 632 * @param create true to create root if not exists 633 * @return The root for tasks 634 */ 635 public ModifiableResourceCollection getCalendarsRoot(Project project, boolean create) 636 { 637 ModifiableResourceCollection moduleRoot = getModuleRoot(project, create); 638 return _getAmetysObject(moduleRoot, __WORKSPACES_CALENDARS_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 639 } 640 641 /** 642 * Get the root for tasks 643 * @param project The project 644 * @param create true to create root if not exists 645 * @return The root for tasks 646 */ 647 public ModifiableResourceCollection getResourceCalendarRoot(Project project, boolean create) 648 { 649 ModifiableResourceCollection moduleRoot = getModuleRoot(project, create); 650 return _getAmetysObject(moduleRoot, __WORKSPACES_RESOURCE_CALENDAR_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create); 651 } 652 653 @Override 654 public Map<String, Object> _getInternalStatistics(Project project, boolean isActive) 655 { 656 if (isActive) 657 { 658 AmetysObjectIterable<Calendar> calendars = getCalendars(project); 659 Calendar ressourceCalendar = getResourceCalendar(project); 660 661 // concatenate both type of calendars 662 long eventNumber = Stream.concat(calendars.stream(), Stream.of(ressourceCalendar)) 663 // get all events for each calendar (events are children of calendar node) 664 .map(TraversableAmetysObject::getChildren) 665 // use flatMap to have a stream with all events from all calendars 666 .flatMap(AmetysObjectIterable::stream) 667 // count the number of events 668 .count(); 669 670 return Map.of(__EVENT_NUMBER_HEADER_ID, eventNumber); 671 } 672 else 673 { 674 return Map.of(__EVENT_NUMBER_HEADER_ID, __SIZE_INACTIVE); 675 } 676 } 677 678 @Override 679 public List<StatisticColumn> _getInternalStatisticModel() 680 { 681 return List.of(new StatisticColumn(__EVENT_NUMBER_HEADER_ID, new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_EVENT_NUMBER")) 682 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderElements") 683 .withType(StatisticsColumnType.LONG) 684 .withGroup(GROUP_HEADER_ELEMENTS_ID)); 685 } 686 687 @Override 688 public Set<String> getAllEventTypes() 689 { 690 return Set.of(ObservationConstants.EVENT_CALENDAR_CREATED, 691 ObservationConstants.EVENT_CALENDAR_DELETED, 692 ObservationConstants.EVENT_CALENDAR_EVENT_CREATED, 693 ObservationConstants.EVENT_CALENDAR_EVENT_DELETED, 694 ObservationConstants.EVENT_CALENDAR_EVENT_UPDATED, 695 ObservationConstants.EVENT_CALENDAR_MOVED, 696 ObservationConstants.EVENT_CALENDAR_RESOURCE_CREATED, 697 ObservationConstants.EVENT_CALENDAR_RESOURCE_DELETED, 698 ObservationConstants.EVENT_CALENDAR_RESOURCE_UPDATED, 699 ObservationConstants.EVENT_CALENDAR_UPDATED); 700 } 701} 702