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.core.user.UserIdentity; 033import org.ametys.plugins.messagingconnector.EventRecurrenceTypeEnum; 034import org.ametys.plugins.repository.AmetysObject; 035import org.ametys.plugins.repository.AmetysRepositoryException; 036import org.ametys.plugins.repository.RepositoryConstants; 037import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 038import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelAwareDataHolder; 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 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(), _getUntilDate()) 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) || eventEndDate.isEqual(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 = _getUntilDate(); 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)) 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 = _getUntilDate(); 477 ZonedDateTime nextDate = RecurrentEventHelper.getNextDate(getRecurrenceType(), getStartDate().withZoneSameInstant(getZone()), occurrence.getStartDate().withZoneSameInstant(getZone())); 478 if (nextDate != null && (untilDate == null || untilDate.isAfter(nextDate))) 479 { 480 return Optional.of(new CalendarEventOccurrence(this, nextDate)); 481 } 482 483 return Optional.empty(); 484 } 485 486 @Override 487 public long getWorkflowId() throws AmetysRepositoryException 488 { 489 try 490 { 491 return getNode().getProperty(PROPERTY_WORKFLOW_ID).getLong(); 492 } 493 catch (RepositoryException e) 494 { 495 throw new AmetysRepositoryException("Unable to get workflowId property", e); 496 } 497 } 498 499 @Override 500 public void setWorkflowId(long workflowId) throws AmetysRepositoryException 501 { 502 Node node = getNode(); 503 504 try 505 { 506 node.setProperty(PROPERTY_WORKFLOW_ID, workflowId); 507 } 508 catch (RepositoryException e) 509 { 510 throw new AmetysRepositoryException("Unable to set workflowId property", e); 511 } 512 } 513 514 @Override 515 public long getCurrentStepId() 516 { 517 throw new UnsupportedOperationException(); 518 } 519 520 @Override 521 public void setCurrentStepId(long stepId) 522 { 523 throw new UnsupportedOperationException(); 524 } 525 526 @Override 527 public void setOrganiser(UserIdentity user) 528 { 529 try 530 { 531 Node creatorNode = null; 532 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER)) 533 { 534 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER); 535 } 536 else 537 { 538 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER, RepositoryConstants.USER_NODETYPE); 539 } 540 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 541 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 542 } 543 catch (RepositoryException e) 544 { 545 throw new AmetysRepositoryException("Error setting the organiser property.", e); 546 } 547 } 548 549 /** 550 * Get attendees to the event 551 * @throws RepositoryException if an error occurred 552 * @return the attendees 553 */ 554 public List<CalendarEventAttendee> getAttendees() throws RepositoryException 555 { 556 List<CalendarEventAttendee> attendees = new ArrayList<>(); 557 558 Node calendarEventNode = getNode(); 559 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 560 { 561 Node attendeesNode = calendarEventNode.getNode(NODE_ATTENDEES_NAME); 562 NodeIterator nodes = attendeesNode.getNodes(); 563 while (nodes.hasNext()) 564 { 565 Node attendeeNode = (Node) nodes.next(); 566 CalendarEventAttendee attendee = new CalendarEventAttendee(); 567 568 boolean isExternal = attendeeNode.getProperty(PROPERTY_ATTENDEE_EXTERNAL).getBoolean(); 569 if (isExternal) 570 { 571 attendee.setEmail(attendeeNode.getProperty(PROPERTY_ATTENDEE_EMAIL).getString()); 572 } 573 else 574 { 575 attendee.setLogin(attendeeNode.getProperty(PROPERTY_ATTENDEE_LOGIN).getString()); 576 attendee.setPopulationId(attendeeNode.getProperty(PROPERTY_ATTENDEE_POPULATION).getString()); 577 } 578 579 attendee.setIsExternal(isExternal); 580 attendee.setIsMandatory(attendeeNode.getProperty(PROPERTY_ATTENDEE_MANDATORY).getBoolean()); 581 582 attendees.add(attendee); 583 } 584 } 585 586 return attendees; 587 } 588 589 /** 590 * Set attendees to the event 591 * @param attendees the list of attendees 592 * @throws RepositoryException if an error occurred 593 */ 594 public void setAttendees(List<CalendarEventAttendee> attendees) throws RepositoryException 595 { 596 Node calendarEventNode = getNode(); 597 598 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 599 { 600 calendarEventNode.getNode(NODE_ATTENDEES_NAME).remove(); 601 } 602 603 Node attendeesNode = calendarEventNode.addNode(NODE_ATTENDEES_NAME, "ametys:unstructured"); 604 for (CalendarEventAttendee attendee : attendees) 605 { 606 // Create new attendee 607 Node attendeeNode = attendeesNode.addNode(NODE_ATTENDEE_NAME, "ametys:unstructured"); 608 if (attendee.isExternal()) 609 { 610 attendeeNode.setProperty(PROPERTY_ATTENDEE_EMAIL, attendee.getEmail()); 611 } 612 else 613 { 614 attendeeNode.setProperty(PROPERTY_ATTENDEE_POPULATION, attendee.getPopulationId()); 615 attendeeNode.setProperty(PROPERTY_ATTENDEE_LOGIN, attendee.getLogin()); 616 } 617 618 attendeeNode.setProperty(PROPERTY_ATTENDEE_EXTERNAL, attendee.isExternal()); 619 attendeeNode.setProperty(PROPERTY_ATTENDEE_MANDATORY, attendee.isMandatory()); 620 } 621 } 622 623 @Override 624 public void setResources(List<String> resources) 625 { 626 setValue(ATTRIBUTE_RESOURCES, resources.toArray(new String[resources.size()])); 627 } 628 629 public ModifiableModelAwareDataHolder getDataHolder() 630 { 631 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode()); 632 return new DefaultModifiableModelAwareDataHolder(repositoryData, _getFactory().getCalendarEventModel()); 633 } 634}