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}