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}