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}