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