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