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