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