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 ZonedDateTime untilAsStored = getValue(ATTRIBUTE_UNTIL_DATE); 258 259 if (untilAsStored != null) 260 { 261 // Because currently user can only choose a date without hour, the until date is stored as midnight in UTC 262 // 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. 263 return getStartDate().withZoneSameInstant(getZone()).with(untilAsStored.toLocalDate()); 264 } 265 else 266 { 267 return null; 268 } 269 } 270 271 @Override 272 public List<ZonedDateTime> getExcludedOccurences() 273 { 274 ZonedDateTime[] excludedOccurences = getValue(ATTRIBUTE_EXCLUDED_DATE, false, new ZonedDateTime[0]); 275 return Arrays.asList(excludedOccurences); 276 } 277 278 @Override 279 public UserIdentity getOrganiser() 280 { 281 return getValue(ATTRIBUTE_ORGANISER); 282 } 283 284 @Override 285 public List<String> getResources() 286 { 287 String[] resources = getValue(ATTRIBUTE_RESOURCES, false, new String[0]); 288 return Arrays.asList(resources); 289 } 290 291 @Override 292 public void setTitle(String title) 293 { 294 setValue(ATTRIBUTE_TITLE, title); 295 } 296 297 @Override 298 public void setDescription(String desc) 299 { 300 setValue(ATTRIBUTE_DESC, desc); 301 } 302 303 @Override 304 public void setLocation(String location) 305 { 306 setValue(ATTRIBUTE_LOCATION, location); 307 } 308 309 310 @Override 311 public void setStartDate(ZonedDateTime startDate) 312 { 313 setValue(ATTRIBUTE_START_DATE, startDate); 314 } 315 316 @Override 317 public void setEndDate(ZonedDateTime endDate) 318 { 319 setValue(ATTRIBUTE_END_DATE, endDate); 320 } 321 322 public void setZone(ZoneId dateZone) 323 { 324 setValue(ATTRIBUTE_DATE_ZONE, dateZone.getId()); 325 } 326 327 @Override 328 public void setFullDay(Boolean fullDay) 329 { 330 setValue(ATTRIBUTE_FULL_DAY, fullDay); 331 } 332 333 @Override 334 public void setCreator(UserIdentity user) 335 { 336 try 337 { 338 Node creatorNode = null; 339 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR)) 340 { 341 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR); 342 } 343 else 344 { 345 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CREATOR, RepositoryConstants.USER_NODETYPE); 346 } 347 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 348 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 349 } 350 catch (RepositoryException e) 351 { 352 throw new AmetysRepositoryException("Error setting the creator property.", e); 353 } 354 } 355 356 @Override 357 public void setCreationDate(ZonedDateTime date) 358 { 359 setValue(ATTRIBUTE_CREATION, date); 360 } 361 362 @Override 363 public void setLastContributor(UserIdentity user) 364 { 365 try 366 { 367 Node creatorNode = null; 368 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR)) 369 { 370 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR); 371 } 372 else 373 { 374 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_CONTRIBUTOR, RepositoryConstants.USER_NODETYPE); 375 } 376 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 377 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 378 } 379 catch (RepositoryException e) 380 { 381 throw new AmetysRepositoryException("Error setting the contributor property.", e); 382 } 383 } 384 385 @Override 386 public void setLastModified(ZonedDateTime date) 387 { 388 setValue(ATTRIBUTE_MODIFIED, date); 389 } 390 391 @Override 392 public void setRecurrenceType(String recurrenceType) 393 { 394 setValue(ATTRIBUTE_RECURRENCE_TYPE, recurrenceType); 395 } 396 397 @Override 398 public void setRepeatUntil(ZonedDateTime untilDate) 399 { 400 if (untilDate == null) 401 { 402 if (hasValue(ATTRIBUTE_UNTIL_DATE)) 403 { 404 removeValue(ATTRIBUTE_UNTIL_DATE); 405 } 406 } 407 else 408 { 409 setValue(ATTRIBUTE_UNTIL_DATE, untilDate); 410 } 411 } 412 413 @Override 414 public void setExcludedOccurrences(List<ZonedDateTime> excludedOccurrences) 415 { 416 setValue(ATTRIBUTE_EXCLUDED_DATE, excludedOccurrences.toArray(new ZonedDateTime[excludedOccurrences.size()])); 417 } 418 419 @Override 420 public List<CalendarEventOccurrence> getOccurrences(ZonedDateTime startDate, ZonedDateTime endDate) 421 { 422 Optional<CalendarEventOccurrence> optionalEvent = getFirstOccurrence(startDate); 423 if (optionalEvent.isPresent() && optionalEvent.get().getStartDate().isBefore(endDate)) 424 { 425 return RecurrentEventHelper.getOccurrences(optionalEvent.get().getStartDate(), endDate, optionalEvent.get().getStartDate(), 426 optionalEvent.get().getStartDate(), getRecurrenceType(), getExcludedOccurences(), getZone(), getRepeatUntil()) 427 .stream() 428 .map(occurrenceStartDate -> new CalendarEventOccurrence(this, occurrenceStartDate)) 429 .collect(Collectors.toList()); 430 } 431 else 432 { 433 return new ArrayList<>(); 434 } 435 } 436 437 @Override 438 public Optional<CalendarEventOccurrence> getFirstOccurrence(ZonedDateTime date) 439 { 440 ZonedDateTime eventStartDate = getStartDate(); 441 ZonedDateTime eventEndDate = getEndDate(); 442 443 if (getFullDay()) 444 { 445 eventEndDate = eventEndDate.plusDays(1); 446 } 447 448 if (eventEndDate.isAfter(date)) 449 { 450 // Return the event himself 451 return Optional.of(new CalendarEventOccurrence(this, eventStartDate)); 452 } 453 else if (this.isRecurrent()) 454 { 455 ZonedDateTime untilDate = getRepeatUntil(); 456 457 long eventDuringTime = ChronoUnit.SECONDS.between(eventStartDate, eventEndDate); 458 459 ZonedDateTime endDate = eventEndDate; 460 ZonedDateTime startDate = eventStartDate; 461 while (startDate != null && endDate.isBefore(date)) 462 { 463 startDate = RecurrentEventHelper.getNextDate(getRecurrenceType(), getStartDate().withZoneSameInstant(getZone()), startDate.withZoneSameInstant(getZone())); 464 if (startDate != null) 465 { 466 endDate = startDate.plusSeconds(eventDuringTime); 467 } 468 } 469 if (untilDate == null || untilDate.isAfter(startDate) || untilDate.equals(startDate)) 470 { 471 return Optional.of(new CalendarEventOccurrence(this, startDate)); 472 } 473 } 474 return Optional.empty(); 475 } 476 477 @Override 478 public Optional<CalendarEventOccurrence> getNextOccurrence(CalendarEventOccurrence occurrence) 479 { 480 ZonedDateTime untilDate = getRepeatUntil(); 481 ZonedDateTime nextDate = RecurrentEventHelper.getNextDate(getRecurrenceType(), getStartDate().withZoneSameInstant(getZone()), occurrence.getStartDate().withZoneSameInstant(getZone())); 482 if (nextDate != null && (untilDate == null || untilDate.isAfter(nextDate) || untilDate.equals(nextDate))) 483 { 484 return Optional.of(new CalendarEventOccurrence(this, nextDate)); 485 } 486 487 return Optional.empty(); 488 } 489 490 @Override 491 public long getWorkflowId() throws AmetysRepositoryException 492 { 493 try 494 { 495 return getNode().getProperty(PROPERTY_WORKFLOW_ID).getLong(); 496 } 497 catch (RepositoryException e) 498 { 499 throw new AmetysRepositoryException("Unable to get workflowId property", e); 500 } 501 } 502 503 @Override 504 public void setWorkflowId(long workflowId) throws AmetysRepositoryException 505 { 506 Node node = getNode(); 507 508 try 509 { 510 node.setProperty(PROPERTY_WORKFLOW_ID, workflowId); 511 } 512 catch (RepositoryException e) 513 { 514 throw new AmetysRepositoryException("Unable to set workflowId property", e); 515 } 516 } 517 518 @Override 519 public long getCurrentStepId() 520 { 521 throw new UnsupportedOperationException(); 522 } 523 524 @Override 525 public void setCurrentStepId(long stepId) 526 { 527 throw new UnsupportedOperationException(); 528 } 529 530 @Override 531 public void setOrganiser(UserIdentity user) 532 { 533 try 534 { 535 Node creatorNode = null; 536 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER)) 537 { 538 creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER); 539 } 540 else 541 { 542 creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + ATTRIBUTE_ORGANISER, RepositoryConstants.USER_NODETYPE); 543 } 544 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin()); 545 creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId()); 546 } 547 catch (RepositoryException e) 548 { 549 throw new AmetysRepositoryException("Error setting the organiser property.", e); 550 } 551 } 552 553 /** 554 * Get attendees to the event 555 * @throws RepositoryException if an error occurred 556 * @return the attendees 557 */ 558 public List<CalendarEventAttendee> getAttendees() throws RepositoryException 559 { 560 List<CalendarEventAttendee> attendees = new ArrayList<>(); 561 562 Node calendarEventNode = getNode(); 563 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 564 { 565 Node attendeesNode = calendarEventNode.getNode(NODE_ATTENDEES_NAME); 566 NodeIterator nodes = attendeesNode.getNodes(); 567 while (nodes.hasNext()) 568 { 569 Node attendeeNode = (Node) nodes.next(); 570 CalendarEventAttendee attendee = new CalendarEventAttendee(); 571 572 boolean isExternal = attendeeNode.getProperty(PROPERTY_ATTENDEE_EXTERNAL).getBoolean(); 573 if (isExternal) 574 { 575 attendee.setEmail(attendeeNode.getProperty(PROPERTY_ATTENDEE_EMAIL).getString()); 576 } 577 else 578 { 579 attendee.setLogin(attendeeNode.getProperty(PROPERTY_ATTENDEE_LOGIN).getString()); 580 attendee.setPopulationId(attendeeNode.getProperty(PROPERTY_ATTENDEE_POPULATION).getString()); 581 } 582 583 attendee.setIsExternal(isExternal); 584 attendee.setIsMandatory(attendeeNode.getProperty(PROPERTY_ATTENDEE_MANDATORY).getBoolean()); 585 586 attendees.add(attendee); 587 } 588 } 589 590 return attendees; 591 } 592 593 /** 594 * Set attendees to the event 595 * @param attendees the list of attendees 596 * @throws RepositoryException if an error occurred 597 */ 598 public void setAttendees(List<CalendarEventAttendee> attendees) throws RepositoryException 599 { 600 Node calendarEventNode = getNode(); 601 602 if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME)) 603 { 604 calendarEventNode.getNode(NODE_ATTENDEES_NAME).remove(); 605 } 606 607 Node attendeesNode = calendarEventNode.addNode(NODE_ATTENDEES_NAME, "ametys:unstructured"); 608 for (CalendarEventAttendee attendee : attendees) 609 { 610 // Create new attendee 611 Node attendeeNode = attendeesNode.addNode(NODE_ATTENDEE_NAME, "ametys:unstructured"); 612 if (attendee.isExternal()) 613 { 614 attendeeNode.setProperty(PROPERTY_ATTENDEE_EMAIL, attendee.getEmail()); 615 } 616 else 617 { 618 attendeeNode.setProperty(PROPERTY_ATTENDEE_POPULATION, attendee.getPopulationId()); 619 attendeeNode.setProperty(PROPERTY_ATTENDEE_LOGIN, attendee.getLogin()); 620 } 621 622 attendeeNode.setProperty(PROPERTY_ATTENDEE_EXTERNAL, attendee.isExternal()); 623 attendeeNode.setProperty(PROPERTY_ATTENDEE_MANDATORY, attendee.isMandatory()); 624 } 625 } 626 627 @Override 628 public void setResources(List<String> resources) 629 { 630 setValue(ATTRIBUTE_RESOURCES, resources.toArray(new String[resources.size()])); 631 } 632 633 public ModifiableIndexableDataHolder getDataHolder() 634 { 635 ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode()); 636 return new DefaultModifiableModelAwareDataHolder(repositoryData, _getFactory().getCalendarEventModel()); 637 } 638}