001/*
002 *  Copyright 2016 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;
017
018import java.text.DateFormat;
019import java.text.SimpleDateFormat;
020import java.time.ZonedDateTime;
021import java.time.temporal.ChronoUnit;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.Date;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Optional;
030import java.util.Set;
031
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.cocoon.components.ContextHelper;
038import org.apache.cocoon.environment.Request;
039import org.apache.commons.collections.ListUtils;
040import org.apache.commons.lang.IllegalClassException;
041
042import org.ametys.core.util.DateUtils;
043import org.ametys.plugins.explorer.ExplorerNode;
044import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
045import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
046import org.ametys.plugins.repository.AmetysObjectIterable;
047import org.ametys.plugins.repository.AmetysObjectIterator;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.UnknownAmetysObjectException;
050import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
051import org.ametys.plugins.repository.query.QueryHelper;
052import org.ametys.plugins.repository.query.SortCriteria;
053import org.ametys.plugins.repository.query.expression.AndExpression;
054import org.ametys.plugins.repository.query.expression.DateExpression;
055import org.ametys.plugins.repository.query.expression.Expression;
056import org.ametys.plugins.repository.query.expression.Expression.Operator;
057import org.ametys.plugins.repository.query.expression.OrExpression;
058import org.ametys.plugins.repository.query.expression.StringExpression;
059import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
060import org.ametys.plugins.workspaces.calendars.Calendar.CalendarVisibility;
061import org.ametys.plugins.workspaces.calendars.events.CalendarEvent;
062import org.ametys.plugins.workspaces.calendars.events.CalendarEventJSONHelper;
063import org.ametys.plugins.workspaces.calendars.events.CalendarEventOccurrence;
064import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEvent;
065import org.ametys.plugins.workspaces.calendars.jcr.JCRCalendarEventFactory;
066import org.ametys.plugins.workspaces.project.objects.Project;
067import org.ametys.runtime.i18n.I18nizableText;
068import org.ametys.web.repository.page.ModifiablePage;
069import org.ametys.web.repository.page.ModifiableZone;
070import org.ametys.web.repository.page.ModifiableZoneItem;
071import org.ametys.web.repository.page.Page;
072import org.ametys.web.repository.page.ZoneItem.ZoneType;
073import org.ametys.web.repository.site.Site;
074import org.ametys.web.repository.sitemap.Sitemap;
075
076import com.google.common.collect.ImmutableSet;
077
078/**
079 * Helper component for managing calendars
080 */
081public class CalendarWorkspaceModule extends AbstractWorkspaceModule implements Configurable
082{
083    /** The id of calendar module */
084    public static final String CALENDAR_MODULE_ID = CalendarWorkspaceModule.class.getName();
085    
086    /** Workspaces calendars node name */
087    private static final String __WORKSPACES_CALENDARS_NODE_NAME = "calendars";
088
089    /** Workspaces root tasks node name */
090    private static final String __WORKSPACES_CALENDARS_ROOT_NODE_NAME = "calendars-root";
091    
092    /** Workspaces root tasks node name */
093    private static final String __WORKSPACES_CALENDAR_RESOURCES_ROOT_NODE_NAME = "calendar-resources-root";
094    
095    /** Workspaces root tasks node name */
096    private static final String __WORKSPACES_RESOURCE_CALENDAR_ROOT_NODE_NAME = "resource-calendar-root";
097    
098    private static final String __CALENDAR_CACHE_REQUEST_ATTR = CalendarWorkspaceModule.class.getName() + "$calendarCache";
099    
100    private CalendarDAO _calendarDAO;
101    private CalendarEventJSONHelper _calendarEventJSONHelper;
102
103    private I18nizableText _defaultCalendarTemplateDesc;
104    private String _defaultCalendarColor;
105    private String _defaultCalendarVisibility;
106    private String _defaultCalendarWorkflowName;
107    private I18nizableText _defaultCalendarTitle;
108    private I18nizableText _defaultCalendarDescription;
109    
110    private I18nizableText _resourceCalendarTemplateDesc;
111    private String _resourceCalendarColor;
112    private String _resourceCalendarVisibility;
113    private String _resourceCalendarWorkflowName;
114    private I18nizableText _resourceCalendarTitle;
115    private I18nizableText _resourceCalendarDescription;
116
117    @Override
118    public void service(ServiceManager manager) throws ServiceException
119    {
120        super.service(manager);
121        _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE);
122        _calendarEventJSONHelper = (CalendarEventJSONHelper) manager.lookup(CalendarEventJSONHelper.ROLE);
123    }
124    
125    public void configure(Configuration configuration) throws ConfigurationException
126    {
127        _defaultCalendarTemplateDesc = I18nizableText.parseI18nizableText(configuration.getChild("template-desc"), "plugin." + _pluginName, "");
128        _defaultCalendarColor = configuration.getChild("color").getValue("col1");
129        _defaultCalendarVisibility = configuration.getChild("visibility").getValue(CalendarVisibility.PRIVATE.name());
130        _defaultCalendarWorkflowName = configuration.getChild("workflow").getValue("calendar-default");
131        _defaultCalendarTitle = I18nizableText.parseI18nizableText(configuration.getChild("title"), "plugin." + _pluginName);
132        _defaultCalendarDescription = I18nizableText.parseI18nizableText(configuration.getChild("description"), "plugin." + _pluginName, "");
133        
134        _resourceCalendarTemplateDesc = I18nizableText.parseI18nizableText(configuration.getChild("resource-template-desc"), "plugin." + _pluginName, "");
135        _resourceCalendarColor = configuration.getChild("resource-color").getValue("resourcecol0");
136        _resourceCalendarVisibility = configuration.getChild("resource-visibility").getValue(CalendarVisibility.PRIVATE.name());
137        _resourceCalendarWorkflowName = configuration.getChild("resource-workflow").getValue("calendar-default");
138        _resourceCalendarTitle = I18nizableText.parseI18nizableText(configuration.getChild("resource-title"), "plugin." + _pluginName);
139        _resourceCalendarDescription = I18nizableText.parseI18nizableText(configuration.getChild("resource-description"), "plugin." + _pluginName, "");
140
141    }
142    
143    @Override
144    public String getId()
145    {
146        return CALENDAR_MODULE_ID;
147    }
148    
149    public int getOrder()
150    {
151        return ORDER_CALENDAR;
152    }
153    
154    public String getModuleName()
155    {
156        return __WORKSPACES_CALENDARS_NODE_NAME;
157    }
158    
159    @Override
160    protected String getModulePageName()
161    {
162        return "calendars";
163    }
164    
165    public I18nizableText getModuleTitle()
166    {
167        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_LABEL");
168    }
169    public I18nizableText getModuleDescription()
170    {
171        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_DESCRIPTION");
172    }
173    @Override
174    protected I18nizableText getModulePageTitle()
175    {
176        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_CALENDARS_TITLE");
177    }
178    
179    @Override
180    protected void initializeModulePage(ModifiablePage calendarPage)
181    {
182        ModifiableZone defaultZone = calendarPage.createZone("default");
183        
184        String serviceId = "org.ametys.plugins.workspaces.module.Calendar";
185        ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
186        defaultZoneItem.setType(ZoneType.SERVICE);
187        defaultZoneItem.setServiceId(serviceId);
188        
189        ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters();
190        serviceDataHolder.setValue("xslt", _getDefaultXslt(serviceId));
191    }
192    
193    /**
194     * Get the calendars of a project
195     * @param project The project
196     * @return The list of calendar
197     */
198    public AmetysObjectIterable<Calendar> getCalendars(Project project)
199    {
200        ModifiableResourceCollection calendarRoot = getCalendarsRoot(project, true);
201        return calendarRoot != null ? calendarRoot.getChildren() : null;
202    }
203    
204    @Override
205    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
206    {
207        try
208        {
209            ExplorerNode projectRootNode = project.getExplorerRootNode();
210            
211            if (projectRootNode instanceof ModifiableResourceCollection)
212            {
213                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
214                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_CALENDARS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
215            }
216            else
217            {
218                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
219            }
220        }
221        catch (AmetysRepositoryException e)
222        {
223            throw new AmetysRepositoryException("Error getting the documents root node.", e);
224        }
225    }
226    
227    /**
228     * Get the URI of a thread in project'site
229     * @param project The project
230     * @param calendarId The id of calendar
231     * @param eventId The id of event
232     * @return The thread uri
233     */
234    public String getEventUri(Project project, String calendarId, String eventId)
235    {
236        String moduleUrl = getModuleUrl(project);
237        if (moduleUrl != null)
238        {
239            StringBuilder sb = new StringBuilder();
240            sb.append(moduleUrl);
241            
242            try
243            {
244                CalendarEvent event = _resolver.resolveById(eventId);
245                
246                if (event.getStartDate() != null)
247                {
248                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
249                    sb.append("?date=").append(df.format(DateUtils.asDate(event.getStartDate())));
250                }
251            }
252            catch (UnknownAmetysObjectException e)
253            {
254                // Nothing
255            }
256            
257            sb.append("#").append(calendarId);
258            
259            return sb.toString();
260        }
261        
262        return null;
263    }
264    
265    /**
266     * Add additional information on project and parent calendar
267     * @param event The event
268     * @param eventData The event data to complete
269     */
270    @SuppressWarnings("unchecked")
271    protected void _addAdditionalEventData(CalendarEvent event, Map<String, Object> eventData)
272    {
273        Request request = ContextHelper.getRequest(_context);
274        
275        Calendar calendar = event.getParent();
276        Project project = _projectManager.getParentProject(calendar);
277        
278        // Try to get calendar from cache if request is not null
279        if (request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR) == null)
280        {
281            request.setAttribute(__CALENDAR_CACHE_REQUEST_ATTR, new HashMap<String, Object>());
282        }
283        
284        Map<String, Object> calendarCache = (Map<String, Object>) request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR);
285        
286        if (!calendarCache.containsKey(calendar.getId()))
287        {
288            Map<String, Object> calendarInfo = new HashMap<>();
289            
290            calendarInfo.put("calendarName", calendar.getName());
291            calendarInfo.put("calendarIsPublic", CalendarVisibility.PUBLIC.equals(calendar.getVisibility()));
292            calendarInfo.put("calendarHasViewRight", canView(calendar));
293            
294            calendarInfo.put("projectId", project.getId());
295            calendarInfo.put("projectTitle", project.getTitle());
296            
297            Set<Page> calendarModulePages = _projectManager.getModulePages(project, this);
298            if (!calendarModulePages.isEmpty())
299            {
300                Page calendarModulePage = calendarModulePages.iterator().next();
301                calendarInfo.put("calendarModulePageId", calendarModulePage.getId());
302            }
303            
304            calendarCache.put(calendar.getId(), calendarInfo);
305        }
306        
307        eventData.putAll((Map<String, Object>) calendarCache.get(calendar.getId()));
308       
309        eventData.put("eventUrl", getEventUri(project, calendar.getId(), event.getId()));
310    }
311    
312    /**
313     * Get the upcoming events of the calendars on which the user has a right
314     * @param months the amount of months from today in which look for upcoming events
315     * @param maxResults the maximum results to display
316     * @param calendarIds the ids of the calendars to gather events from, null for all calendars
317     * @param tagIds the ids of the valid tags for the events, null for any tag 
318     * @return the upcoming events
319     */
320    public List<Map<String, Object>> getUpcomingEvents(int months, int maxResults, List<String> calendarIds, List<String> tagIds)
321    {
322        List<Map<String, Object>> basicEventList = new ArrayList<> ();
323        List<Map<String, Object>> recurrentEventList = new ArrayList<> ();
324        
325        java.util.Calendar cal = java.util.Calendar.getInstance();
326        cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
327        cal.set(java.util.Calendar.MINUTE, 0);
328        cal.set(java.util.Calendar.SECOND, 0);
329        cal.set(java.util.Calendar.MILLISECOND, 0);
330        ZonedDateTime startDate = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS);
331
332        ZonedDateTime endDate = startDate.plusMonths(months);
333        
334        Expression nonRecurrentExpr = new StringExpression(JCRCalendarEvent.ATTRIBUTE_RECURRENCE_TYPE, Operator.EQ, "NEVER");
335        Expression startDateExpr = new DateExpression(JCRCalendarEvent.ATTRIBUTE_START_DATE, Operator.GE, DateUtils.asDate(startDate));
336        Expression endDateExpr = new DateExpression(JCRCalendarEvent.ATTRIBUTE_START_DATE, Operator.LE, DateUtils.asDate(endDate));
337        
338        Expression keywordsExpr = null;
339        
340        if (tagIds != null && !tagIds.isEmpty())
341        {
342            List<Expression> orExpr = new ArrayList<>();
343            for (String tagId : tagIds)
344            {
345                orExpr.add(new StringExpression(JCRCalendarEvent.ATTRIBUTE_KEYWORDS, Operator.EQ, tagId));
346            }
347            keywordsExpr = new OrExpression(orExpr.toArray(new Expression[orExpr.size()]));
348        }
349        
350        // Get the non recurrent events sorted by ascending date and within the configured range
351        Expression eventExpr = new AndExpression(nonRecurrentExpr, startDateExpr, endDateExpr, keywordsExpr);
352        SortCriteria sortCriteria = new SortCriteria();
353        sortCriteria.addCriterion(JCRCalendarEvent.ATTRIBUTE_START_DATE, true, false);
354        
355        String basicEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
356        AmetysObjectIterable<CalendarEvent> basicEvents = _resolver.query(basicEventQuery);
357        AmetysObjectIterator<CalendarEvent> basicEventIt = basicEvents.iterator();
358        
359        int processed = 0;
360        while (basicEventIt.hasNext() && processed < maxResults)
361        {
362            CalendarEvent event = basicEventIt.next();
363            Calendar holdingCalendar = (Calendar) event.getParent();
364            
365            if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
366            {
367                // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
368                
369                // FIXME should use something like an EventInfo object with some data + calendar, project name
370                // And use a function to process the transformation...
371                // Function<EventInfo, Map<String, Object>> fn. eventData.putAll(fn.apply(info));
372                
373                // standard set of event data
374                Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJson(event, false);
375                basicEventList.add(eventData);
376                processed++;
377                
378                // add additional info
379                _addAdditionalEventData(event, eventData);
380            }
381        }
382        
383        Expression recurrentExpr = new StringExpression(JCRCalendarEvent.ATTRIBUTE_RECURRENCE_TYPE, Operator.NE, "NEVER");
384        eventExpr = new AndExpression(recurrentExpr, keywordsExpr);
385        
386        String recurrentEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
387        AmetysObjectIterable<CalendarEvent> recurrentEvents = _resolver.query(recurrentEventQuery);
388        AmetysObjectIterator<CalendarEvent> recurrentEventIt = recurrentEvents.iterator();
389        
390        // FIXME cannot count processed here...
391        processed = 0;
392        while (recurrentEventIt.hasNext() /*&& processed < maxResultsAsInt*/)
393        {
394            CalendarEvent event = recurrentEventIt.next();
395            Optional<CalendarEventOccurrence> nextOccurrence = event.getNextOccurrence(new CalendarEventOccurrence(event, startDate));
396            
397            // The recurrent event first occurrence is within the range
398            if (nextOccurrence.isPresent() && nextOccurrence.get().before(endDate))
399            {
400                // FIXME calculate occurrences only if keep event...
401                List<CalendarEventOccurrence> occurrences = event.getOccurrences(nextOccurrence.get().getStartDate(), endDate);
402                Calendar holdingCalendar = (Calendar) event.getParent();
403                
404                if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
405                {
406                    // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
407                    
408                    // Add all its occurrences that are within the range
409                    for (CalendarEventOccurrence occurrence : occurrences)
410                    {
411                        Map<String, Object> eventData = _calendarEventJSONHelper.eventAsJsonWithOccurrence(event, occurrence.getStartDate(), false);
412                        recurrentEventList.add(eventData);
413                        processed++;
414                        
415                        _addAdditionalEventData(event, eventData);
416                        
417                    }
418                }
419            }
420        }
421        
422        // Re-sort chronologically the events' union
423        List<Map<String, Object>> allEvents = ListUtils.union(basicEventList, recurrentEventList);
424        Collections.sort(allEvents, new StartDateComparator());
425
426        // Return the first maxResults events
427        return allEvents.size() <= maxResults ? allEvents : allEvents.subList(0, maxResults);
428    }
429    
430    /**
431     * Determine whether the given event has to be kept or not depending on the given calendars
432     * @param calendarIds the ids of the calendars
433     * @param event the event
434     * @return true if the event can be kept, false otherwise
435     */
436    private boolean _filterEvent(List<String> calendarIds, CalendarEvent event)
437    {
438        Calendar holdingCalendar = (Calendar) event.getParent();
439        // FIXME calendarIds.get(0) == null means "All calendars" selected in the select calendar widget ??
440        // need cleaner code
441        return calendarIds == null || calendarIds.get(0) == null || calendarIds.contains(holdingCalendar.getId());
442    }
443    
444    private boolean _hasAccess(Calendar calendar)
445    {
446        return CalendarVisibility.PUBLIC.equals(calendar.getVisibility()) || canView(calendar);
447    }
448        
449    /**
450     * Indicates if the current user can view the calendar
451     * @param calendar The calendar to test
452     * @return true if the calendar can be viewed 
453     */
454    public boolean canView(Calendar calendar)
455    {
456        return _rightManager.currentUserHasReadAccess(calendar);
457    }
458    
459    /**
460     * Indicates if the current user can view the event
461     * @param event The event to test
462     * @return true if the event can be viewed 
463     */
464    public boolean canView(CalendarEvent event)
465    {
466        return _rightManager.currentUserHasReadAccess(event.getParent());
467    }
468    
469    /**
470     * Compares events on their starting date
471     */
472    protected static class StartDateComparator implements Comparator<Map<String, Object>>
473    {
474        @Override
475        public int compare(Map<String, Object> calendarEventInfo1, Map<String, Object> calendarEventInfo2)
476        {
477            String startDate1asString = (String) calendarEventInfo1.get("startDate");
478            String startDate2asString = (String) calendarEventInfo2.get("startDate");
479            
480            Date startDate1 = DateUtils.parse(startDate1asString);
481            Date startDate2 = DateUtils.parse(startDate2asString);
482            
483            // The start date is before if 
484            return startDate1.compareTo(startDate2);
485        }
486    }
487    
488    @Override
489    public Set<String> getAllowedEventTypes()
490    {
491        return ImmutableSet.of("calendar.event.created", "calendar.event.updated");
492    }
493
494    @Override
495    protected void _internalActivateModule(Project project, Map<String, Object> additionalValues)
496    {
497        createResourceCalendar(project, additionalValues);
498        _createDefaultCalendar(project, additionalValues);
499    }
500
501    /**
502     * Create a calendar to store resources if needed
503     * @param project the project
504     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language
505     * @return The resource calendar
506     */
507    public Calendar createResourceCalendar(Project project, Map<String, Object> additionalValues)
508    {
509        ModifiableResourceCollection resourceCalendarRoot = getResourceCalendarRoot(project, true);
510        
511        String lang;
512        if (additionalValues.containsKey("language"))
513        {
514            lang = (String) additionalValues.get("language");
515        }
516        else
517        {
518            lang = getLang(project);
519        }
520
521        Calendar resourceCalendar = resourceCalendarRoot.getChildren()
522                .stream()
523                .filter(Calendar.class::isInstance)
524                .map(Calendar.class::cast)
525                .findFirst()
526                .orElse(null);
527        
528        if (resourceCalendar == null)
529        {
530            Boolean renameIfExists = false;
531            Boolean checkRights = false;
532            String description = _i18nUtils.translate(_resourceCalendarDescription, lang);
533            String inputName = _i18nUtils.translate(_resourceCalendarTitle, lang);
534            String templateDesc = _i18nUtils.translate(_resourceCalendarTemplateDesc, lang);
535            try
536            {
537                Map result = _calendarDAO.addCalendar(resourceCalendarRoot, inputName, description, templateDesc, _resourceCalendarColor, _resourceCalendarVisibility, _resourceCalendarWorkflowName, renameIfExists, checkRights, false);
538
539                resourceCalendar = _resolver.resolveById((String) result.get("id"));
540            }
541            catch (Exception e)
542            {
543                getLogger().error("Error while trying to create the first calendar in a newly created project", e);
544            }
545        }
546        return resourceCalendar;
547    }
548    
549    private void _createDefaultCalendar(Project project, Map<String, Object> additionalValues)
550    {
551        ModifiableResourceCollection moduleRoot = getCalendarsRoot(project, true);
552        
553        if (moduleRoot != null && !_hasOtherCalendar(project))
554        {
555            Boolean renameIfExists = false;
556            Boolean checkRights = false;
557
558            String lang;
559            if (additionalValues.containsKey("language"))
560            {
561                lang = (String) additionalValues.get("language");
562            }
563            else
564            {
565                lang = getLang(project);
566            }
567            
568            String description = _i18nUtils.translate(_defaultCalendarDescription, lang);
569            String inputName = _i18nUtils.translate(_defaultCalendarTitle, lang);
570            String templateDesc = _i18nUtils.translate(_defaultCalendarTemplateDesc, lang);
571            try
572            {
573                _calendarDAO.addCalendar(moduleRoot, inputName, description, templateDesc, _defaultCalendarColor, _defaultCalendarVisibility, _defaultCalendarWorkflowName, renameIfExists, checkRights, false);
574            }
575            catch (Exception e)
576            {
577                getLogger().error("Error while trying to create the first calendar in a newly created project", e);
578            }
579            
580        }
581    }
582    
583    private boolean _hasOtherCalendar(Project project)
584    {
585        AmetysObjectIterable<Calendar> calendars = getCalendars(project);
586        
587        return calendars != null && calendars.getSize() > 0;
588    }
589    
590    /**
591     * Get the lang for this project, or fr if none found
592     * @param project the project to check
593     * @return a lang in this project
594     */
595    protected String getLang(Project project)
596    {
597        /*
598         * get the sites, then the sitemaps (transformed back to a stream of sitemap)
599         * then get the name
600         * return the 1st one, or "fr" by default if none is available
601         */
602        return project.getSites()
603                .stream()
604                .sequential()
605                .map(Site::getSitemaps)
606                .flatMap(AmetysObjectIterable::stream)
607                .map(Sitemap::getName)
608                .findFirst()
609                .orElse("fr");
610    }
611
612    /**
613     * Get the calendars of a project
614     * @param project The project
615     * @return The list of calendar
616     */
617    public Calendar getResourceCalendar(Project project)
618    {
619        ModifiableResourceCollection resourceCalendarRoot = getResourceCalendarRoot(project, true);
620        return resourceCalendarRoot.getChildren()
621                .stream()
622                .filter(Calendar.class::isInstance)
623                .map(Calendar.class::cast)
624                .findFirst()
625                .orElse(createResourceCalendar(project, new HashMap<String, Object>()));
626    }
627
628    /**
629     * Get the root for tasks
630     * @param project The project
631     * @param create true to create root if not exists
632     * @return The root for tasks
633     */
634    public ModifiableResourceCollection getCalendarResourcesRoot(Project project, boolean create)
635    {
636        ModifiableResourceCollection moduleRoot = getModuleRoot(project, create);
637        return _getAmetysObject(moduleRoot, __WORKSPACES_CALENDAR_RESOURCES_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
638    }
639
640    /**
641     * Get the root for tasks
642     * @param project The project
643     * @param create true to create root if not exists
644     * @return The root for tasks
645     */
646    public ModifiableResourceCollection getCalendarsRoot(Project project, boolean create)
647    {
648        ModifiableResourceCollection moduleRoot = getModuleRoot(project, create);
649        return _getAmetysObject(moduleRoot, __WORKSPACES_CALENDARS_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
650    }
651
652    /**
653     * Get the root for tasks
654     * @param project The project
655     * @param create true to create root if not exists
656     * @return The root for tasks
657     */
658    public ModifiableResourceCollection getResourceCalendarRoot(Project project, boolean create)
659    {
660        ModifiableResourceCollection moduleRoot = getModuleRoot(project, create);
661        return _getAmetysObject(moduleRoot, __WORKSPACES_RESOURCE_CALENDAR_ROOT_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
662    }
663}
664