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}