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