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