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.explorer.calendars.jcr;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Calendar;
021import java.util.Date;
022import java.util.GregorianCalendar;
023import java.util.List;
024
025import javax.jcr.Node;
026import javax.jcr.NodeIterator;
027import javax.jcr.RepositoryException;
028
029import org.ametys.core.user.UserIdentity;
030import org.ametys.plugins.explorer.calendars.CalendarEvent;
031import org.ametys.plugins.explorer.calendars.CalendarEventAttendee;
032import org.ametys.plugins.explorer.calendars.EventRecurrenceTypeEnum;
033import org.ametys.plugins.explorer.calendars.ModifiableCalendarEvent;
034import org.ametys.plugins.explorer.calendars.helper.RecurrentEventHelper;
035import org.ametys.plugins.repository.AmetysObject;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.RepositoryConstants;
038import org.ametys.plugins.repository.jcr.DefaultTraversableAmetysObject;
039import org.ametys.plugins.repository.metadata.CompositeMetadata;
040import org.ametys.plugins.repository.metadata.UnknownMetadataException;
041
042/**
043 * Default implementation of an {@link CalendarEvent}, backed by a JCR node.<br>
044 */
045public class JCRCalendarEvent extends DefaultTraversableAmetysObject<JCRCalendarEventFactory> implements ModifiableCalendarEvent
046{
047    /** Metadata's name for author's event* */
048    public static final String METADATA_CREATOR = "creator";
049    
050    /** Metadata's name for lastModified's event* */
051    public static final String METADATA_CREATION = "creationDate";
052
053    /** Metadata's name for last contributor's event* */
054    public static final String METADATA_CONTRIBUTOR = "contributor";
055    
056    /** Metadata's name for lastModified's event* */
057    public static final String METADATA_MODIFIED = "lastModified";
058    
059    /** Metadata's name for title's event* */
060    public static final String METADATA_TITLE = "title";
061    
062    /** Metadata's name for description's event* */
063    public static final String METADATA_DESC = "description";
064    
065    /** Metadata's name for keywords' event* */
066    public static final String METADATA_KEYWORDS = "keywords";
067    
068    /** Metadata's name for location's event* */
069    public static final String METADATA_LOCATION = "location";
070    
071    /** Metadata's name for startDate's event* */
072    public static final String METADATA_START_DATE = "startDate";
073    
074    /** Metadata's name for endDate's event* */
075    public static final String METADATA_END_DATE = "endDate";
076    
077    /** Metadata's name for last validated date's event* */
078    public static final String METADATA_VALIDATED = "lastValidated";
079    
080    /** Metadata's name for last validator's event* */
081    public static final String METADATA_VALIDATOR = "validator";
082    
083    /** Metadata's name for fullDay's event* */
084    public static final String METADATA_FULL_DAY = "fullDay";
085    
086    /** Metadata's name for recurrence type event* */
087    public static final String METADATA_RECURRENCE_TYPE = "recurrenceType";
088    
089    /** Metadata's name for until date event* */
090    public static final String METADATA_UNTIL_DATE = "untilDate";
091    
092    /** Metadata's name for excluded date event* */
093    public static final String METADATA_EXCLUDED_DATE = "excludedDate";
094    
095    /** Metadata's name for organiser's event* */
096    public static final String METADATA_ORGANISER = "organiser";
097    
098    /** Property name for attendee population * */
099    public static final String PROPERTY_ATTENDEE_POPULATION = "populationId";
100    
101    /** Property name for attendee login * */
102    public static final String PROPERTY_ATTENDEE_LOGIN = "login";
103    
104    /** Property name for attendee email * */
105    public static final String PROPERTY_ATTENDEE_EMAIL = "email";
106    
107    /** Property name for attendee external * */
108    public static final String PROPERTY_ATTENDEE_EXTERNAL = "external";
109    
110    /** Property name for attendee mandatory * */
111    public static final String PROPERTY_ATTENDEE_MANDATORY = "mandatory";
112    
113    /** Name of the node for attendees * */
114    public static final String NODE_ATTENDEES_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":calendar-event-attendees";
115    
116    /** Name of the node for attendee * */
117    public static final String NODE_ATTENDEE_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":calendar-event-attendee";
118    
119    
120    /** Property's name for workflow id */
121    public static final String PROPERTY_WORKFLOW_ID = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowId";
122    
123    /**
124     * Creates an {@link JCRCalendarEvent}.
125     * @param node the node backing this {@link AmetysObject}
126     * @param parentPath the parentPath in the Ametys hierarchy
127     * @param factory the DefaultAmetysObjectFactory which created the AmetysObject
128     */
129    public JCRCalendarEvent(Node node, String parentPath, JCRCalendarEventFactory factory)
130    {
131        super(node, parentPath, factory);
132    }
133
134    @Override
135    public String getTitle()
136    {
137        return getMetadataHolder().getString(METADATA_TITLE);
138    }
139    
140    @Override
141    public String getDescription()
142    {
143        return getMetadataHolder().getString(METADATA_DESC);
144    }
145    
146    @Override
147    public String getLocation()
148    {
149        return getMetadataHolder().getString(METADATA_LOCATION, null);
150    }
151    
152    @Override
153    public String[] getKeywords() throws AmetysRepositoryException
154    {
155        return getMetadataHolder().getStringArray(METADATA_KEYWORDS, new String[0]);
156    }
157    
158    @Override
159    public Date getStartDate()
160    {
161        return getMetadataHolder().getDate(METADATA_START_DATE);
162    }
163
164    @Override
165    public Date getEndDate()
166    {
167        return getMetadataHolder().getDate(METADATA_END_DATE);
168    }
169
170    @Override
171    public Boolean getFullDay()
172    {
173        return getMetadataHolder().getBoolean(METADATA_FULL_DAY);
174    }
175    
176    @Override
177    public UserIdentity getCreator()
178    {
179        CompositeMetadata creatorMetadata = getMetadataHolder().getCompositeMetadata(METADATA_CREATOR);
180        return new UserIdentity(creatorMetadata.getString("login"), creatorMetadata.getString("population"));
181    }
182
183    @Override
184    public Date getCreationDate()
185    {
186        return getMetadataHolder().getDate(METADATA_CREATION);
187    }
188
189    @Override
190    public UserIdentity getLastContributor()
191    {
192        CompositeMetadata contributorMetadata = getMetadataHolder().getCompositeMetadata(METADATA_CONTRIBUTOR);
193        return new UserIdentity(contributorMetadata.getString("login"), contributorMetadata.getString("population"));
194    }
195
196    @Override
197    public Date getLastModified()
198    {
199        return getMetadataHolder().getDate(METADATA_MODIFIED);
200    }
201    
202    @Override
203    public UserIdentity getLastValidator() throws UnknownMetadataException, AmetysRepositoryException
204    {
205        if (getMetadataHolder().hasMetadata(METADATA_VALIDATOR))
206        {
207            CompositeMetadata validatorMetadata = getMetadataHolder().getCompositeMetadata(METADATA_VALIDATOR);
208            return new UserIdentity(validatorMetadata.getString("login"), validatorMetadata.getString("population"));
209        }
210        
211        return null;
212    }
213    
214    @Override
215    public Date getLastValidated() throws UnknownMetadataException, AmetysRepositoryException
216    {
217        if (getMetadataHolder().hasMetadata(METADATA_VALIDATED))
218        {
219            return getMetadataHolder().getDate(METADATA_VALIDATED);
220        }
221        
222        return null;
223    }
224    
225    @Override
226    public EventRecurrenceTypeEnum getRecurrenceType()
227    {
228        EventRecurrenceTypeEnum recurrenceEnum = EventRecurrenceTypeEnum.NEVER;
229        if (getMetadataHolder().hasMetadata(METADATA_RECURRENCE_TYPE))
230        {
231            String recurrenceType = getMetadataHolder().getString(METADATA_RECURRENCE_TYPE);
232            try
233            {
234                recurrenceEnum = EventRecurrenceTypeEnum.valueOf(recurrenceType);
235            }
236            catch (Exception e)
237            {
238                // ignore
239            }
240        }
241        
242        return recurrenceEnum;
243    }
244    
245    @Override
246    public Boolean isRecurrent()
247    {
248        return !getRecurrenceType().equals(EventRecurrenceTypeEnum.NEVER);
249    }
250    
251    @Override
252    public Date getRepeatUntil()
253    {
254        if (getMetadataHolder().hasMetadata(METADATA_UNTIL_DATE))
255        {
256            return getMetadataHolder().getDate(METADATA_UNTIL_DATE);
257        }
258        
259        return null;
260    }
261    
262    @Override
263    public List<Date> getExcludedOccurences()
264    {
265        List<Date> excludedOccurences = new ArrayList<>();
266        if (getMetadataHolder().hasMetadata(METADATA_EXCLUDED_DATE))
267        {
268            excludedOccurences = Arrays.asList(getMetadataHolder().getDateArray(METADATA_EXCLUDED_DATE));
269        }
270        return excludedOccurences;
271    }
272    
273    @Override
274    public UserIdentity getOrganiser() throws UnknownMetadataException, AmetysRepositoryException
275    {
276        if (getMetadataHolder().hasMetadata(METADATA_ORGANISER))
277        {
278            CompositeMetadata organiserMetadata = getMetadataHolder().getCompositeMetadata(METADATA_ORGANISER);
279            return new UserIdentity(organiserMetadata.getString("login"), organiserMetadata.getString("population"));
280        }
281        
282        return null;
283    }
284    
285    @Override
286    public void setTitle(String title)
287    {
288        getMetadataHolder().setMetadata(METADATA_TITLE, title);
289    }
290    
291    @Override
292    public void setDescription(String desc)
293    {
294        getMetadataHolder().setMetadata(METADATA_DESC, desc);
295    }
296    
297    @Override
298    public void setLocation(String location)
299    {
300        getMetadataHolder().setMetadata(METADATA_LOCATION, location);
301    }
302    
303    @Override
304    public void setKeywords(String[] keywords)
305    {
306        getMetadataHolder().setMetadata(METADATA_KEYWORDS, keywords);
307    }
308    
309    @Override
310    public void setStartDate(Date startDate)
311    {
312        getMetadataHolder().setMetadata(METADATA_START_DATE, startDate);
313    }
314
315    @Override
316    public void setEndDate(Date endDate)
317    {
318        getMetadataHolder().setMetadata(METADATA_END_DATE, endDate);     
319    }
320
321    @Override
322    public void setFullDay(Boolean fullDay)
323    {
324        getMetadataHolder().setMetadata(METADATA_FULL_DAY, fullDay);
325    }
326    
327    @Override
328    public void setCreator(UserIdentity user) throws UnknownMetadataException, AmetysRepositoryException
329    {
330        try
331        {
332            Node creatorNode = null;
333            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CREATOR))
334            {
335                creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CREATOR);
336            }
337            else
338            {
339                creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CREATOR, RepositoryConstants.USER_NODETYPE);
340            }
341            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin());
342            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId());
343        }
344        catch (RepositoryException e)
345        {
346            throw new AmetysRepositoryException("Error setting the creator property.", e);
347        }
348    }
349
350    @Override
351    public void setCreationDate(Date date) throws UnknownMetadataException, AmetysRepositoryException
352    {
353        getMetadataHolder().setMetadata(METADATA_CREATION, date);
354    }
355
356    @Override
357    public void setLastContributor(UserIdentity user) throws UnknownMetadataException, AmetysRepositoryException
358    {
359        try
360        {
361            Node creatorNode = null;
362            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CONTRIBUTOR))
363            {
364                creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CONTRIBUTOR);
365            }
366            else
367            {
368                creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_CONTRIBUTOR, RepositoryConstants.USER_NODETYPE);
369            }
370            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin());
371            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId());
372        }
373        catch (RepositoryException e)
374        {
375            throw new AmetysRepositoryException("Error setting the contributor property.", e);
376        }
377    }
378
379    @Override
380    public void setLastModified(Date date) throws UnknownMetadataException, AmetysRepositoryException
381    {
382        getMetadataHolder().setMetadata(METADATA_MODIFIED, date);
383    }
384    
385    @Override
386    public void setLastValidator(UserIdentity user) throws UnknownMetadataException, AmetysRepositoryException
387    {
388        try
389        {
390            Node creatorNode = null;
391            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_VALIDATOR))
392            {
393                creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_VALIDATOR);
394            }
395            else
396            {
397                creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_VALIDATOR, RepositoryConstants.USER_NODETYPE);
398            }
399            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin());
400            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId());
401        }
402        catch (RepositoryException e)
403        {
404            throw new AmetysRepositoryException("Error setting the validator property.", e);
405        }
406    }
407    
408    @Override
409    public void setLastValidated(Date date) throws UnknownMetadataException, AmetysRepositoryException
410    {
411        getMetadataHolder().setMetadata(METADATA_VALIDATED, date);
412    }
413    
414    @Override
415    public void setRecurrenceType(String recurrenceType)
416    {
417        getMetadataHolder().setMetadata(METADATA_RECURRENCE_TYPE, recurrenceType);
418    }
419    
420    @Override
421    public void setRepeatUntil(Date untilDate)
422    {
423        if (untilDate == null)
424        {
425            if (getMetadataHolder().hasMetadata(METADATA_UNTIL_DATE))
426            {
427                getMetadataHolder().removeMetadata(METADATA_UNTIL_DATE);
428            }
429        }
430        else
431        {
432            getMetadataHolder().setMetadata(METADATA_UNTIL_DATE, untilDate);
433        }
434    }
435    
436    @Override
437    public void setExcludedOccurrences(List<Date> excludedOccurrences)
438    {
439        getMetadataHolder().setMetadata(METADATA_EXCLUDED_DATE, excludedOccurrences.toArray(new Date[excludedOccurrences.size()]));
440    }
441    
442    @Override
443    public List<Date> getOccurrences(Date startDate, Date endDate)
444    {
445        Date untilDate = getRepeatUntil();
446        
447        // until date must be included in list of occurrences.
448        // For that purpose, until date is used here as a threshold value that
449        // must be set to the start of the next day
450        if (untilDate != null)
451        {
452            Calendar cal = Calendar.getInstance();
453            cal.setTime(untilDate);
454            cal.add(Calendar.DATE, 1);
455            cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
456            cal.set(java.util.Calendar.MINUTE, 0);
457            cal.set(java.util.Calendar.SECOND, 0);
458            cal.set(java.util.Calendar.MILLISECOND, 0);
459            untilDate = cal.getTime();
460        }
461        
462        List<Date> occurences = new ArrayList<>();
463        Date firstDate = getFirstOccurrence(startDate);
464        if (firstDate == null || (untilDate != null && !firstDate.before(untilDate)))
465        {
466            return occurences;
467        }
468        
469        List<Date> excludedOccurences = getExcludedOccurences();
470        
471        GregorianCalendar firstDateCalendar = new GregorianCalendar();
472        firstDateCalendar.setTime(firstDate);
473        firstDateCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
474        firstDateCalendar.set(java.util.Calendar.MINUTE, 0);
475        firstDateCalendar.set(java.util.Calendar.SECOND, 0);
476        firstDateCalendar.set(java.util.Calendar.MILLISECOND, 0);
477        
478        if (!excludedOccurences.contains(firstDateCalendar.getTime()))
479        {
480            occurences.add(firstDate);
481        }
482        
483        Date nextDate = getNextOccurrence(firstDate);
484        while (nextDate != null && nextDate.before(endDate) && (untilDate == null || untilDate.after(nextDate)))
485        {
486            GregorianCalendar nextDateCalendar = new GregorianCalendar();
487            nextDateCalendar.setTime(nextDate);
488            nextDateCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
489            nextDateCalendar.set(java.util.Calendar.MINUTE, 0);
490            nextDateCalendar.set(java.util.Calendar.SECOND, 0);
491            nextDateCalendar.set(java.util.Calendar.MILLISECOND, 0);
492            
493            if (!excludedOccurences.contains(nextDateCalendar.getTime()))
494            {
495                occurences.add(nextDate);
496            }
497            nextDate = getNextOccurrence(nextDate);
498        }
499        
500        return occurences;
501    }
502    
503    @Override
504    public Date getFirstOccurrence(Date date)
505    {
506        Date eventStartDate = getStartDate();
507        Date eventEndDate = getEndDate();
508        
509        if (eventEndDate.after(date))
510        {
511            return eventStartDate;
512        }
513        else
514        {
515            long eventDuringTime = eventEndDate.getTime() - eventStartDate.getTime();
516            
517            Date endDate = eventEndDate;
518            Date startDate = eventStartDate;
519            while (startDate != null && endDate.before(date))
520            {
521                startDate = RecurrentEventHelper.getNextDate(this, startDate);
522                if (startDate != null)
523                {
524                    endDate = new Date(startDate.getTime() + eventDuringTime);
525                }
526            }
527            
528            return startDate;
529        }
530    }
531    
532    @Override
533    public Date getNextOccurrence(Date date)
534    {
535        return RecurrentEventHelper.getNextDate(this, date);
536    }
537    
538    @Override
539    public long getWorkflowId() throws AmetysRepositoryException
540    {
541        try
542        {
543            return getNode().getProperty(PROPERTY_WORKFLOW_ID).getLong();
544        }
545        catch (RepositoryException e)
546        {
547            throw new AmetysRepositoryException("Unable to get workflowId property", e);
548        }
549    }
550    
551    @Override
552    public void setWorkflowId(long workflowId) throws AmetysRepositoryException
553    {
554        Node node = getNode();
555        
556        try
557        {
558            node.setProperty(PROPERTY_WORKFLOW_ID, workflowId);
559        }
560        catch (RepositoryException e)
561        {
562            throw new AmetysRepositoryException("Unable to set workflowId property", e);
563        }
564    }
565    
566    @Override
567    public long getCurrentStepId()
568    {
569        throw new UnsupportedOperationException();
570    }
571    
572    @Override
573    public void setCurrentStepId(long stepId)
574    {
575        throw new UnsupportedOperationException();
576    }
577    
578    @Override
579    public void setOrganiser(UserIdentity user)
580    {
581        try
582        {
583            Node creatorNode = null;
584            if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_ORGANISER))
585            {
586                creatorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_ORGANISER);
587            }
588            else
589            {
590                creatorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_ORGANISER, RepositoryConstants.USER_NODETYPE);
591            }
592            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", user.getLogin());
593            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", user.getPopulationId());
594        }
595        catch (RepositoryException e)
596        {
597            throw new AmetysRepositoryException("Error setting the organiser property.", e);
598        }
599    }
600    
601    /**
602     * Get attendees to the event
603     * @throws RepositoryException if an error occurred
604     * @return the attendees
605     */
606    public List<CalendarEventAttendee> getAttendees() throws RepositoryException 
607    {
608        List<CalendarEventAttendee> attendees = new ArrayList<>();
609        
610        Node calendarEventNode = getNode();
611        if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME))
612        {
613            Node attendeesNode = calendarEventNode.getNode(NODE_ATTENDEES_NAME);
614            NodeIterator nodes = attendeesNode.getNodes();
615            while (nodes.hasNext())
616            {
617                Node attendeeNode = (Node) nodes.next();
618                CalendarEventAttendee attendee = new CalendarEventAttendee();
619                
620                boolean isExternal = attendeeNode.getProperty(PROPERTY_ATTENDEE_EXTERNAL).getBoolean();
621                if (isExternal)
622                {
623                    attendee.setEmail(attendeeNode.getProperty(PROPERTY_ATTENDEE_EMAIL).getString());
624                }
625                else
626                {
627                    attendee.setLogin(attendeeNode.getProperty(PROPERTY_ATTENDEE_LOGIN).getString());
628                    attendee.setPopulationId(attendeeNode.getProperty(PROPERTY_ATTENDEE_POPULATION).getString());
629                }
630                
631                attendee.setIsExternal(isExternal);
632                attendee.setIsMandatory(attendeeNode.getProperty(PROPERTY_ATTENDEE_MANDATORY).getBoolean());
633                
634                attendees.add(attendee);
635            }
636        }
637        
638        return attendees;
639    }
640    
641    /**
642     * Set attendees to the event
643     * @param attendees the list of attendees
644     * @throws RepositoryException if an error occurred
645     */
646    public void setAttendees(List<CalendarEventAttendee> attendees) throws RepositoryException 
647    {
648        Node calendarEventNode = getNode();
649        
650        if (calendarEventNode.hasNode(NODE_ATTENDEES_NAME))
651        {
652            calendarEventNode.getNode(NODE_ATTENDEES_NAME).remove();
653        }
654        
655        Node attendeesNode = calendarEventNode.addNode(NODE_ATTENDEES_NAME, "ametys:unstructured");
656        for (CalendarEventAttendee attendee : attendees)
657        {
658            // Create new attendee
659            Node attendeeNode = attendeesNode.addNode(NODE_ATTENDEE_NAME, "ametys:unstructured");
660            if (attendee.isExternal())
661            {
662                attendeeNode.setProperty(PROPERTY_ATTENDEE_EMAIL, attendee.getEmail());
663            }
664            else
665            {
666                attendeeNode.setProperty(PROPERTY_ATTENDEE_POPULATION, attendee.getPopulationId());
667                attendeeNode.setProperty(PROPERTY_ATTENDEE_LOGIN, attendee.getLogin());
668            }
669            
670            attendeeNode.setProperty(PROPERTY_ATTENDEE_EXTERNAL, attendee.isExternal());
671            attendeeNode.setProperty(PROPERTY_ATTENDEE_MANDATORY, attendee.isMandatory());
672        }
673    }
674
675}