001/* 002 * Copyright 2014 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.jcr; 017 018import java.time.ZoneId; 019import java.time.ZonedDateTime; 020import java.time.temporal.ChronoUnit; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.List; 024import java.util.Optional; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import javax.jcr.Node; 029import javax.jcr.NodeIterator; 030import javax.jcr.RepositoryException; 031 032import org.ametys.cms.repository.DefaultIndexableTraversableAmetysObject; 033import org.ametys.core.user.UserIdentity; 034import org.ametys.plugins.messagingconnector.EventRecurrenceTypeEnum; 035import org.ametys.plugins.repository.AmetysObject; 036import org.ametys.plugins.repository.AmetysRepositoryException; 037import org.ametys.plugins.repository.RepositoryConstants; 038import org.ametys.plugins.repository.data.holder.ModifiableDataHolder; 039import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableDataHolder; 040import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData; 041import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData; 042import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper; 043import org.ametys.plugins.workflow.repository.WorkflowAwareAmetysObject; 044import org.ametys.plugins.workspaces.calendars.Calendar; 045import org.ametys.plugins.workspaces.calendars.events.CalendarEvent; 046import org.ametys.plugins.workspaces.calendars.events.CalendarEventAttendee; 047import org.ametys.plugins.workspaces.calendars.events.CalendarEventOccurrence; 048import org.ametys.plugins.workspaces.calendars.events.ModifiableCalendarEvent; 049import org.ametys.plugins.workspaces.calendars.helper.RecurrentEventHelper; 050 051/** 052 * Default implementation of an {@link CalendarEvent}, backed by a JCR node.<br> 053 */ 054public class JCRCalendarEvent extends DefaultIndexableTraversableAmetysObject<JCRCalendarEventFactory> implements ModifiableCalendarEvent, WorkflowAwareAmetysObject 055{ 056 057 /** Attribute name for event author*/ 058 public static final String ATTRIBUTE_CREATOR = "creator"; 059 060 /** Attribute name for event lastModified*/ 061 public static final String ATTRIBUTE_CREATION = "creationDate"; 062 063 /** Attribute name for event last contributor*/ 064 public static final String ATTRIBUTE_CONTRIBUTOR = "contributor"; 065 066 /** Attribute name for event lastModified*/ 067 public static final String ATTRIBUTE_MODIFIED = "lastModified"; 068 069 /** Attribute name for event title */ 070 public static final String ATTRIBUTE_TITLE = "title"; 071 072 /** Attribute name for event description*/ 073 public static final String ATTRIBUTE_DESC = "description"; 074 075 /** Attribute name for event keywords' */ 076 public static final String ATTRIBUTE_KEYWORDS = "keywords"; 077 078 /** Attribute name for event location*/ 079 public static final String ATTRIBUTE_LOCATION = "location"; 080 081 /** Attribute name for event startDate*/ 082 public static final String ATTRIBUTE_START_DATE = "startDate"; 083 084 /** Attribute name for event endDate*/ 085 public static final String ATTRIBUTE_END_DATE = "endDate"; 086 087 /** Attribute name for event date sone*/ 088 public static final String ATTRIBUTE_DATE_ZONE = "dateZone"; 089 090 /** Attribute name for event fullDay*/ 091 public static final String ATTRIBUTE_FULL_DAY = "fullDay"; 092 093 /** Attribute name for event recurrence type */ 094 public static final String ATTRIBUTE_RECURRENCE_TYPE = "recurrenceType"; 095 096 /** Attribute name for event until date */ 097 public static final String ATTRIBUTE_UNTIL_DATE = "untilDate"; 098 099 /** Attribute name for event excluded date */ 100 public static final String ATTRIBUTE_EXCLUDED_DATE = "excludedDate"; 101 102 /** Attribute name for event organiser */ 103 public static final String ATTRIBUTE_ORGANISER = "organiser"; 104 105 /** Property name for attendee population * */ 106 public static final String PROPERTY_ATTENDEE_POPULATION = "populationId"; 107 108 /** Property name for attendee login * */ 109 public static final String PROPERTY_ATTENDEE_LOGIN = "login"; 110 111 /** Property name for attendee email * */ 112 public static final String PROPERTY_ATTENDEE_EMAIL = "email"; 113 114 /** Property name for attendee external * */ 115 public static final String PROPERTY_ATTENDEE_EXTERNAL = "external"; 116 117 /** Property name for attendee mandatory * */ 118 public static final String PROPERTY_ATTENDEE_MANDATORY = "mandatory"; 119 120 /** Name of the node for attendees * */ 121 public static final String NODE_ATTENDEES_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":calendar-event-attendees"; 122 123 /** Name of the node for attendee * */ 124 public static final String NODE_ATTENDEE_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":calendar-event-attendee"; 125 126 /** Property's name for resources */ 127 public static final String ATTRIBUTE_RESOURCES = "resources"; 128 129 /** 130 * Creates an {@link JCRCalendarEvent}. 131 * @param node the node backing this {@link AmetysObject} 132 * @param parentPath the parentPath in the Ametys hierarchy 133 * @param factory the DefaultAmetysObjectFactory which created the AmetysObject 134 */ 135 public JCRCalendarEvent(Node node, String parentPath, JCRCalendarEventFactory factory) 136 { 137 super(node, parentPath, factory); 138 } 139 140 public Calendar getCalendar() 141 { 142 return getParent(); 143 } 144 145 @Override 146 public String getTitle() 147 { 148 return getValue(ATTRIBUTE_TITLE); 149 } 150 151 @Override 152 public String getDescription() 153 { 154 return getValue(ATTRIBUTE_DESC); 155 } 156 157 @Override 158 public String getLocation() 159 { 160 return getValueOrDefault(ATTRIBUTE_LOCATION, null); 161 } 162 163 public void tag(String tag) throws AmetysRepositoryException 164 { 165 TaggableAmetysObjectHelper.tag(this, tag); 166 } 167 168 public void untag(String tag) throws AmetysRepositoryException 169 { 170 TaggableAmetysObjectHelper.untag(this, tag); 171 } 172 173 public Set<String> getTags() throws AmetysRepositoryException 174 { 175 return TaggableAmetysObjectHelper.getTags(this); 176 } 177 178 @Override 179 public ZonedDateTime getStartDate() 180 { 181 return getValue(ATTRIBUTE_START_DATE); 182 } 183 184 @Override 185 public ZonedDateTime getEndDate() 186 { 187 return getValue(ATTRIBUTE_END_DATE); 188 } 189 190 @Override 191 public ZoneId getZone() 192 { 193 if (hasValue(ATTRIBUTE_DATE_ZONE)) 194 { 195 String zoneId = getValue(ATTRIBUTE_DATE_ZONE); 196 if (ZoneId.of(zoneId) != null) 197 { 198 return ZoneId.of(zoneId); 199 } 200 } 201 202 // For legacy purposes: old events won't have any zone, in this case, use system default zone id 203 return ZoneId.systemDefault(); 204 } 205 206 @Override 207 public Boolean getFullDay() 208 { 209 return getValue(ATTRIBUTE_FULL_DAY); 210 } 211 212 @Override 213 public UserIdentity getCreator() 214 { 215 return getValue(ATTRIBUTE_CREATOR); 216 } 217 218 @Override 219 public ZonedDateTime getCreationDate() 220 { 221 return getValue(ATTRIBUTE_CREATION); 222 } 223 224 @Override 225 public UserIdentity getLastContributor() 226 { 227 return getValue(ATTRIBUTE_CONTRIBUTOR); 228 } 229 230 @Override 231 public ZonedDateTime getLastModified() 232 { 233 return getValue(ATTRIBUTE_MODIFIED); 234 } 235 236 @Override 237 public EventRecurrenceTypeEnum getRecurrenceType() 238 { 239 String recurrenceType = getValueOrDefault(ATTRIBUTE_RECURRENCE_TYPE, EventRecurrenceTypeEnum.NEVER.toString()); 240 EventRecurrenceTypeEnum recurrenceEnum = EventRecurrenceTypeEnum.valueOf(recurrenceType); 241 return recurrenceEnum; 242 } 243 244 @Override 245 public Boolean isRecurrent() 246 { 247 return !getRecurrenceType().equals(EventRecurrenceTypeEnum.NEVER); 248 } 249 250 @Override 251 public ZonedDateTime getRepeatUntil() 252 { 253 ZonedDateTime untilAsStored = getValue(ATTRIBUTE_UNTIL_DATE); 254 255 if (untilAsStored != null) 256 { 257 // Because currently user can only choose a date without hour, the until date is stored as midnight in UTC 258 // We need to convert it to the event timezone and set the hour to the starting hour of the event so that an occurrence on the same day is included. 259 return getStartDate().withZoneSameInstant(getZone()).with(untilAsStored.toLocalDate()); 260 } 261 else 262 { 263 return null; 264 } 265 } 266 267 @Override 268 public List<ZonedDateTime> getExcludedOccurences() 269 { 270 ZonedDateTime[] excludedOccurences = getValueOrDefault(ATTRIBUTE_EXCLUDED_DATE, new ZonedDateTime[0]); 271 return Arrays.asList(excludedOccurences); 272 } 273 274 @Override 275 public UserIdentity getOrganiser() 276 { 277 return getValue(ATTRIBUTE_ORGANISER); 278 } 279 280 @Override 281 public List<String> getResources() 282 { 283 String[] resources = getValueOrDefault(ATTRIBUTE_RESOURCES, new String[0]); 284 return Arrays.asList(resources); 285 } 286 287 @Override 288 public void setTitle(String title) 289 { 290 setValue(ATTRIBUTE_TITLE, title); 291 } 292 293 @Override 294 public void setDescription(String desc) 295 { 296 setValue(ATTRIBUTE_DESC, desc); 297 } 298 299 @Override 300 public void setLocation(String location) 301 { 302 setValue(ATTRIBUTE_LOCATION, location); 303 } 304 305 306 @Override 307 public void setStartDate(ZonedDateTime startDate) 308 { 309 setValue(ATTRIBUTE_START_DATE, startDate); 310 } 311 312 @Override 313 public void setEndDate(ZonedDateTime endDate) 314 { 315 setValue(ATTRIBUTE_END_DATE, endDate); 316 } 317 318 public void setZone(ZoneId dateZone) 319 { 320 setValue(ATTRIBUTE_DATE_ZONE, dateZone.getId()); 321 } 322 323 @Override 324 public void setFullDay(Boolean fullDay) 325 { 326 setValue(ATTRIBUTE_FULL_DAY, fullDay); 327 } 328 329 @Override 330 public void setCreator(UserIdentity user) 331 { 332 try 333 { 334 Node creatorNode = null; 335 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR)) 336 { 337 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR); 338 } 339 else 340 { 341 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR, RepositoryConstants.USER_NODETYPE); 342 } 343 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 344 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 345 } 346 catch (RepositoryException e) 347 { 348 throw new AmetysRepositoryException("Error setting the creator property.", e); 349 } 350 } 351 352 @Override 353 public void setCreationDate(ZonedDateTime date) 354 { 355 setValue(ATTRIBUTE_CREATION, date); 356 } 357 358 @Override 359 public void setLastContributor(UserIdentity user) 360 { 361 try 362 { 363 Node creatorNode = null; 364 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR)) 365 { 366 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR); 367 } 368 else 369 { 370 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR, RepositoryConstants.USER_NODETYPE); 371 } 372 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 373 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 374 } 375 catch (RepositoryException e) 376 { 377 throw new AmetysRepositoryException("Error setting the contributor property.", e); 378 } 379 } 380 381 @Override 382 public void setLastModified(ZonedDateTime date) 383 { 384 setValue(ATTRIBUTE_MODIFIED, date); 385 } 386 387 @Override 388 public void setRecurrenceType(String recurrenceType) 389 { 390 setValue(ATTRIBUTE_RECURRENCE_TYPE, recurrenceType); 391 } 392 393 @Override 394 public void setRepeatUntil(ZonedDateTime untilDate) 395 { 396 if (untilDate == null) 397 { 398 if (hasValue(ATTRIBUTE_UNTIL_DATE)) 399 { 400 removeValue(ATTRIBUTE_UNTIL_DATE); 401 } 402 } 403 else 404 { 405 setValue(ATTRIBUTE_UNTIL_DATE, untilDate); 406 } 407 } 408 409 @Override 410 public void setExcludedOccurrences(List<ZonedDateTime> excludedOccurrences) 411 { 412 setValue(ATTRIBUTE_EXCLUDED_DATE, excludedOccurrences.toArray(new ZonedDateTime[excludedOccurrences.size()])); 413 } 414 415 @Override 416 public List<CalendarEventOccurrence> getOccurrences(ZonedDateTime startDate, ZonedDateTime endDate) 417 { 418 Optional<CalendarEventOccurrence> optionalEvent = getFirstOccurrence(startDate); 419 if (optionalEvent.isPresent() && optionalEvent.get().getStartDate().isBefore(endDate)) 420 { 421 return RecurrentEventHelper.getOccurrences(optionalEvent.get().getStartDate(), endDate, optionalEvent.get().getStartDate(), 422 optionalEvent.get().getStartDate(), getRecurrenceType(), getExcludedOccurences(), getZone(), getRepeatUntil()) 423 .stream() 424 .map(occurrenceStartDate -> new CalendarEventOccurrence(this, occurrenceStartDate)) 425 .collect(Collectors.toList()); 426 } 427 else 428 { 429 return new ArrayList<>(); 430 } 431 } 432 433 @Override 434 public Optional<CalendarEventOccurrence> getFirstOccurrence(ZonedDateTime date) 435 { 436 ZonedDateTime eventStartDate = getStartDate(); 437 ZonedDateTime eventEndDate = getEndDate(); 438 439 if (getFullDay()) 440 { 441 eventEndDate = eventEndDate.plusDays(1); 442 } 443 444 if (eventEndDate.isAfter(date)) 445 { 446 // Return the event himself 447 return Optional.of(new CalendarEventOccurrence(this, eventStartDate)); 448 } 449 else if (this.isRecurrent()) 450 { 451 ZonedDateTime untilDate = getRepeatUntil(); 452 453 long eventDuringTime = ChronoUnit.SECONDS.between(eventStartDate, eventEndDate); 454 455 ZonedDateTime endDate = eventEndDate; 456 ZonedDateTime startDate = eventStartDate; 457 while (startDate != null && endDate.isBefore(date)) 458 { 459 startDate = RecurrentEventHelper.getNextDate(getRecurrenceType(), getStartDate().withZoneSameInstant(getZone()), startDate.withZoneSameInstant(getZone())); 460 if (startDate != null) 461 { 462 endDate = startDate.plusSeconds(eventDuringTime); 463 } 464 } 465 if (untilDate == null || untilDate.isAfter(startDate) || untilDate.equals(startDate)) 466 { 467 return Optional.of(new CalendarEventOccurrence(this, startDate)); 468 } 469 } 470 return Optional.empty(); 471 } 472 473 @Override 474 public Optional<CalendarEventOccurrence> getNextOccurrence(CalendarEventOccurrence occurrence) 475 { 476 ZonedDateTime untilDate = getRepeatUntil(); 477 ZonedDateTime nextDate = RecurrentEventHelper.getNextDate(getRecurrenceType(), getStartDate().withZoneSameInstant(getZone()), occurrence.getStartDate().withZoneSameInstant(getZone())); 478 if (nextDate != null && (untilDate == null || untilDate.isAfter(nextDate) || untilDate.equals(nextDate))) 479 { 480 return Optional.of(new CalendarEventOccurrence(this, nextDate)); 481 } 482 483 return Optional.empty(); 484 } 485 486 @Override 487 public long getCurrentStepId() 488 { 489 throw new UnsupportedOperationException(); 490 } 491 492 @Override 493 public void setCurrentStepId(long stepId) 494 { 495 throw new UnsupportedOperationException(); 496 } 497 498 @Override 499 public void setOrganiser(UserIdentity user) 500 { 501 try 502 { 503 Node creatorNode = null; 504 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER)) 505 { 506 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER); 507 } 508 else 509 { 510 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER, RepositoryConstants.USER_NODETYPE); 511 } 512 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 513 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 514 } 515 catch (RepositoryException e) 516 { 517 throw new AmetysRepositoryException("Error setting the organiser property.", e); 518 } 519 } 520 521 /** 522 * Get attendees to the event 523 * @throws RepositoryException if an error occurred 524 * @return the attendees 525 */ 526 public List<CalendarEventAttendee> getAttendees() throws RepositoryException 527 { 528 List<CalendarEventAttendee> attendees = new ArrayList<>(); 529 530 Node calendarEventNode = getNode(); 531 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 532 { 533 Node attendeesNode = calendarEventNode.getNode(NODE_ATTENDEES_NAME); 534 NodeIterator nodes = attendeesNode.getNodes(); 535 while (nodes.hasNext()) 536 { 537 Node attendeeNode = (Node) nodes.next(); 538 CalendarEventAttendee attendee = new CalendarEventAttendee(); 539 540 boolean isExternal = attendeeNode.getProperty(PROPERTY_ATTENDEE_EXTERNAL).getBoolean(); 541 if (isExternal) 542 { 543 attendee.setEmail(attendeeNode.getProperty(PROPERTY_ATTENDEE_EMAIL).getString()); 544 } 545 else 546 { 547 attendee.setLogin(attendeeNode.getProperty(PROPERTY_ATTENDEE_LOGIN).getString()); 548 attendee.setPopulationId(attendeeNode.getProperty(PROPERTY_ATTENDEE_POPULATION).getString()); 549 } 550 551 attendee.setIsExternal(isExternal); 552 attendee.setIsMandatory(attendeeNode.getProperty(PROPERTY_ATTENDEE_MANDATORY).getBoolean()); 553 554 attendees.add(attendee); 555 } 556 } 557 558 return attendees; 559 } 560 561 /** 562 * Set attendees to the event 563 * @param attendees the list of attendees 564 * @throws RepositoryException if an error occurred 565 */ 566 public void setAttendees(List<CalendarEventAttendee> attendees) throws RepositoryException 567 { 568 Node calendarEventNode = getNode(); 569 570 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 571 { 572 calendarEventNode.getNode(NODE_ATTENDEES_NAME).remove(); 573 } 574 575 Node attendeesNode = calendarEventNode.addNode(NODE_ATTENDEES_NAME, "ametys:unstructured"); 576 for (CalendarEventAttendee attendee : attendees) 577 { 578 // Create new attendee 579 Node attendeeNode = attendeesNode.addNode(NODE_ATTENDEE_NAME, "ametys:unstructured"); 580 if (attendee.isExternal()) 581 { 582 attendeeNode.setProperty(PROPERTY_ATTENDEE_EMAIL, attendee.getEmail()); 583 } 584 else 585 { 586 attendeeNode.setProperty(PROPERTY_ATTENDEE_POPULATION, attendee.getPopulationId()); 587 attendeeNode.setProperty(PROPERTY_ATTENDEE_LOGIN, attendee.getLogin()); 588 } 589 590 attendeeNode.setProperty(PROPERTY_ATTENDEE_EXTERNAL, attendee.isExternal()); 591 attendeeNode.setProperty(PROPERTY_ATTENDEE_MANDATORY, attendee.isMandatory()); 592 } 593 } 594 595 @Override 596 public void setResources(List<String> resources) 597 { 598 setValue(ATTRIBUTE_RESOURCES, resources.toArray(new String[resources.size()])); 599 } 600 601 public ModifiableDataHolder getWorkflowMetadataHolder() 602 { 603 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL); 604 return new DefaultModifiableDataHolder(_getFactory().getInternalDataTypesExtensionPoint(), repositoryData); 605 } 606}