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