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.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.Set;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034import org.apache.commons.collections.ListUtils;
035import org.apache.commons.lang.IllegalClassException;
036import org.apache.commons.lang3.StringUtils;
037
038import org.ametys.cms.transformation.xslt.ResolveURIComponent;
039import org.ametys.core.right.RightManager.RightResult;
040import org.ametys.core.ui.Callable;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.core.util.DateUtils;
043import org.ametys.plugins.explorer.ExplorerNode;
044import org.ametys.plugins.explorer.calendars.Calendar;
045import org.ametys.plugins.explorer.calendars.Calendar.CalendarVisibility;
046import org.ametys.plugins.explorer.calendars.CalendarEvent;
047import org.ametys.plugins.explorer.calendars.actions.CalendarDAO;
048import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEvent;
049import org.ametys.plugins.explorer.calendars.jcr.JCRCalendarEventFactory;
050import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
051import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectIterator;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
057import org.ametys.plugins.repository.query.QueryHelper;
058import org.ametys.plugins.repository.query.SortCriteria;
059import org.ametys.plugins.repository.query.expression.AndExpression;
060import org.ametys.plugins.repository.query.expression.DateExpression;
061import org.ametys.plugins.repository.query.expression.Expression;
062import org.ametys.plugins.repository.query.expression.Expression.Operator;
063import org.ametys.plugins.repository.query.expression.OrExpression;
064import org.ametys.plugins.repository.query.expression.StringExpression;
065import org.ametys.plugins.workspaces.AbstractWorkspaceModule;
066import org.ametys.plugins.workspaces.project.objects.Project;
067import org.ametys.runtime.i18n.I18nizableText;
068import org.ametys.web.repository.page.ModifiablePage;
069import org.ametys.web.repository.page.ModifiableZone;
070import org.ametys.web.repository.page.ModifiableZoneItem;
071import org.ametys.web.repository.page.Page;
072import org.ametys.web.repository.page.ZoneItem.ZoneType;
073
074import com.google.common.collect.ImmutableSet;
075
076/**
077 * Helper component for managing calendars
078 */
079public class CalendarWorkspaceModule extends AbstractWorkspaceModule
080{
081    /** The id of calendar module */
082    public static final String CALENDAR_MODULE_ID = CalendarWorkspaceModule.class.getName();
083    
084    /** Tag on the main page holding the calendar module */
085    private static final String __CALENDAR_MODULE_TAG = "WORKSPACES_MODULE_CALENDAR";
086    
087    /** Workspaces calendars node name */
088    private static final String __WORKSPACES_CALENDARS_NODE_NAME = "calendars";
089    
090    private static final String __CALENDAR_CACHE_REQUEST_ATTR = CalendarWorkspaceModule.class.getName() + "$calendarCache";
091    
092    /** Module i18n title key */
093    private static final String __MODULE_TITLE_KEY = "PLUGINS_WORKSPACES_PROJECT_SERVICE_MODULE_CALENDAR_LABEL";
094
095    private WorkspaceCalendarDAO _calendarDAO;
096    
097    @Override
098    public void service(ServiceManager manager) throws ServiceException
099    {
100        super.service(manager);
101        _calendarDAO = (WorkspaceCalendarDAO) manager.lookup(WorkspaceCalendarDAO.ROLE);
102    }
103    
104    @Override
105    public String getId()
106    {
107        return CALENDAR_MODULE_ID;
108    }
109    
110    public String getModuleName()
111    {
112        return __WORKSPACES_CALENDARS_NODE_NAME;
113    }
114    
115    @Override
116    public I18nizableText getModuleTitle()
117    {
118        return new I18nizableText("plugin." + _pluginName, __MODULE_TITLE_KEY);
119    }
120    
121    @Override
122    protected String getModulePageName()
123    {
124        return "calendars";
125    }
126    
127    @Override
128    protected I18nizableText getModulePageTitle()
129    {
130        return new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_WORKSPACE_PAGE_CALENDARS_TITLE");
131    }
132    
133    @Override
134    protected String getModuleTagName()
135    {
136        return __CALENDAR_MODULE_TAG;
137    }
138    
139    @Override
140    protected void initializeModulePage(ModifiablePage calendarPage)
141    {
142        ModifiableZone defaultZone = calendarPage.createZone("default");
143        
144        String serviceId = "org.ametys.plugins.workspaces.module.Calendar";
145        ModifiableZoneItem defaultZoneItem = defaultZone.addZoneItem();
146        defaultZoneItem.setType(ZoneType.SERVICE);
147        defaultZoneItem.setServiceId(serviceId);
148        
149        ModifiableModelAwareDataHolder serviceDataHolder = defaultZoneItem.getServiceParameters();
150        serviceDataHolder.setValue("xslt", _getDefaultXslt(serviceId));
151    }
152    
153    /**
154     * Get the calendars of a project
155     * @param project The project
156     * @return The list of calendar
157     */
158    public AmetysObjectIterable<Calendar> getCalendars(Project project)
159    {
160        ModifiableResourceCollection moduleRoot = getModuleRoot(project, false);
161        return moduleRoot != null ? moduleRoot.getChildren() : null;
162    }
163    
164    @Override
165    public ModifiableResourceCollection getModuleRoot(Project project, boolean create)
166    {
167        try
168        {
169            ExplorerNode projectRootNode = project.getExplorerRootNode();
170            
171            if (projectRootNode instanceof ModifiableResourceCollection)
172            {
173                ModifiableResourceCollection projectRootNodeRc = (ModifiableResourceCollection) projectRootNode;
174                return _getAmetysObject(projectRootNodeRc, __WORKSPACES_CALENDARS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE, create);
175            }
176            else
177            {
178                throw new IllegalClassException(ModifiableResourceCollection.class, projectRootNode.getClass());
179            }
180        }
181        catch (AmetysRepositoryException e)
182        {
183            throw new AmetysRepositoryException("Error getting the documents root node.", e);
184        }
185    }
186
187    /**
188     * Retrieves the set of general rights used in the calendar module for the current user
189     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
190     */
191    @Callable
192    public Map<String, Object> getModuleBaseRights()
193    {
194        Request request = ContextHelper.getRequest(_context);
195        String projectName = (String) request.getAttribute("projectName");
196        
197        Project project = _projectManager.getProject(projectName);
198        ModifiableResourceCollection calendarRoot = getModuleRoot(project, false);
199        
200        Map<String, Object> rightsData = new HashMap<>();
201        UserIdentity user = _currentUserProvider.getUser();
202        
203        // Add calendar
204        rightsData.put("add-calendar", calendarRoot != null && _rightManager.hasRight(user, CalendarDAO.RIGHTS_CALENDAR_ADD, calendarRoot) == RightResult.RIGHT_ALLOW);
205        
206        // Tags
207        rightsData.put("add-tag", _projectRightHelper.canAddTag(project));
208        rightsData.put("remove-tag", _projectRightHelper.canRemoveTag(project));
209        
210        // Places
211        rightsData.put("add-place", _projectRightHelper.canAddPlace(project));
212        rightsData.put("remove-place", _projectRightHelper.canRemovePlace(project));
213        
214        return rightsData;
215    }
216    
217    /**
218     * Get the rights of the current user for the calendar service
219     * @param calendarIds The list of calendars
220     * @return The rights
221     */
222    @Callable
223    public Map<String, Object> getCalendarServiceRights(List<String> calendarIds)
224    {
225        Map<String, Object> rights = new HashMap<>();
226        
227        List<String> rightEventAdd = new ArrayList<>();
228        List<String> rightTagAdd = new ArrayList<>();
229        List<String> rightPlaceAdd = new ArrayList<>();
230        
231        UserIdentity currentUser = _currentUserProvider.getUser();
232        
233        List<String> ids = calendarIds;
234        if (calendarIds == null || (calendarIds.size() == 1 && calendarIds.get(0) == null))
235        {
236            ids = getCalendarsData().stream().map(c -> (String) c.get("id")).collect(Collectors.toList());
237        }
238        
239        for (String calendarId : ids)
240        {
241            Calendar calendar = _resolver.resolveById(calendarId);
242            
243            if (_rightManager.hasRight(currentUser, CalendarDAO.RIGHTS_EVENT_ADD, calendar) == RightResult.RIGHT_ALLOW)
244            {
245                rightEventAdd.add(calendarId);
246            }
247        }
248        
249        rights.put("eventAdd", rightEventAdd);
250        rights.put("tagAdd", rightTagAdd);
251        rights.put("placetAdd", rightPlaceAdd);
252        
253        return rights;
254    }
255    
256    /**
257     * Get the URI of a thread in project'site
258     * @param project The project
259     * @param calendarId The id of calendar
260     * @param eventId The id of event
261     * @param language The sitemap language
262     * @return The thread uri
263     */
264    public String getEventUri(Project project, String calendarId, String eventId, String language)
265    {
266        AmetysObjectIterable<Page> pages = getModulePages(project, language);
267        
268        if (pages.getSize() > 0)
269        {
270            Page page = pages.iterator().next();
271            
272            StringBuilder sb = new StringBuilder();
273            sb.append(ResolveURIComponent.resolve("page", page.getId()));
274            
275            try
276            {
277                CalendarEvent event = _resolver.resolveById(eventId);
278                
279                if (event.getStartDate() != null)
280                {
281                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
282                    sb.append("?date=").append(df.format(event.getStartDate()));
283                }
284            }
285            catch (UnknownAmetysObjectException e)
286            {
287                // Nothing
288            }
289            
290            sb.append("#").append(calendarId);
291            
292            return sb.toString();
293        }
294        
295        return null;
296    }
297    
298    /**
299     * Add a calendar
300     * @param inputName The desired name for the calendar
301     * @param description The calendar description
302     * @param templateDesc The calendar template description
303     * @param color The calendar color
304     * @param visibility The calendar visibility
305     * @param workflowName The calendar workflow name
306     * @param renameIfExists True to rename if existing
307     * @return The result map with id, parentId and name keys
308     * @throws IllegalAccessException If the user has no sufficient rights
309     */
310    @Callable
311    public Map<String, Object> addCalendar(String inputName, String description, String templateDesc, String color, String visibility, String workflowName, Boolean renameIfExists) throws IllegalAccessException
312    {
313        Request request = ContextHelper.getRequest(_context);
314        String projectName = (String) request.getAttribute("projectName");
315        
316        Project project = _projectManager.getProject(projectName);
317        ModifiableResourceCollection calendarRoot = getModuleRoot(project, false);
318        assert calendarRoot != null;
319        
320        // TODO catch IllegalAccessException -> error = has-right
321        
322        return _calendarDAO.addCalendar(calendarRoot.getId(), inputName, description, templateDesc, color, visibility, workflowName, renameIfExists);
323    }
324    
325    /**
326     * Add additional information on project and parent calendar
327     * @param event The event
328     * @param eventData The event data to complete
329     */
330    @SuppressWarnings("unchecked")
331    protected void _addAdditionalEventData(CalendarEvent event, Map<String, Object> eventData)
332    {
333        Request request = ContextHelper.getRequest(_context);
334        String language = (String) request.getAttribute("sitemapLanguage");
335        
336        Calendar calendar = event.getParent();
337        Project project = _projectManager.getParentProject(calendar);
338        
339        // Try to get calendar from cache if request is not null
340        if (request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR) == null)
341        {
342            request.setAttribute(__CALENDAR_CACHE_REQUEST_ATTR, new HashMap<String, Object>());
343        }
344        
345        Map<String, Object> calendarCache = (Map<String, Object>) request.getAttribute(__CALENDAR_CACHE_REQUEST_ATTR);
346        
347        if (!calendarCache.containsKey(calendar.getId()))
348        {
349            Map<String, Object> calendarInfo = new HashMap<>();
350            
351            calendarInfo.put("calendarName", calendar.getName());
352            calendarInfo.put("calendarIsPublic", CalendarVisibility.PUBLIC.equals(calendar.getVisibility()));
353            calendarInfo.put("calendarHasViewRight", canView(calendar));
354            
355            calendarInfo.put("projectId", project.getId());
356            calendarInfo.put("projectTitle", project.getTitle());
357            
358            AmetysObjectIterable<Page> calendarModulePages = getModulePages(project, language);
359            if (calendarModulePages.getSize() > 0)
360            {
361                Page calendarModulePage = calendarModulePages.iterator().next();
362                calendarInfo.put("calendarModulePageId", calendarModulePage.getId());
363            }
364            
365            calendarCache.put(calendar.getId(), calendarInfo);
366        }
367        
368        eventData.putAll((Map<String, Object>) calendarCache.get(calendar.getId()));
369       
370        eventData.put("eventUrl", getEventUri(project, calendar.getId(), event.getId(), language));
371    }
372    
373    
374    
375    /**
376     * Get the upcoming events of the calendars on which the user has a right
377     * @param months the amount of months from today in which look for upcoming events
378     * @param maxResults the maximum results to display
379     * @param calendarIds the ids of the calendars to gather events from, null for all calendars
380     * @param tagIds the ids of the valid tags for the events, null for any tag 
381     * @return the upcoming events
382     */
383    @Callable
384    public List<Map<String, Object>> getUpcomingEvents(String months, String maxResults, List<String> calendarIds, List<String> tagIds)
385    {
386        int monthsAsInt = StringUtils.isBlank(months) ? 3 : Integer.parseInt(months);
387        int maxResultsAsInt = StringUtils.isBlank(maxResults) ? Integer.MAX_VALUE : Integer.parseInt(maxResults);
388        
389        return getUpcomingEvents(monthsAsInt, maxResultsAsInt, calendarIds, tagIds);
390    }
391    
392    /**
393     * Get the upcoming events of the calendars on which the user has a right
394     * @param months the amount of months from today in which look for upcoming events
395     * @param maxResults the maximum results to display
396     * @param calendarIds the ids of the calendars to gather events from, null for all calendars
397     * @param tagIds the ids of the valid tags for the events, null for any tag 
398     * @return the upcoming events
399     */
400    public List<Map<String, Object>> getUpcomingEvents(int months, int maxResults, List<String> calendarIds, List<String> tagIds)
401    {
402        List<Map<String, Object>> basicEventList = new ArrayList<> ();
403        List<Map<String, Object>> recurrentEventList = new ArrayList<> ();
404        
405        java.util.Calendar cal = java.util.Calendar.getInstance();
406        cal.set(java.util.Calendar.HOUR_OF_DAY, 0);
407        cal.set(java.util.Calendar.MINUTE, 0);
408        cal.set(java.util.Calendar.SECOND, 0);
409        cal.set(java.util.Calendar.MILLISECOND, 0);
410        Date startDate = cal.getTime();
411
412        Date endDate = org.apache.commons.lang3.time.DateUtils.addMonths(startDate, months);
413        
414        Expression nonRecurrentExpr = new StringExpression(JCRCalendarEvent.METADATA_RECURRENCE_TYPE, Operator.EQ, "NEVER");
415        Expression startDateExpr = new DateExpression(JCRCalendarEvent.METADATA_START_DATE, Operator.GE, startDate);
416        Expression endDateExpr = new DateExpression(JCRCalendarEvent.METADATA_START_DATE, Operator.LE, endDate);
417        
418        Expression keywordsExpr = null;
419        
420        if (tagIds != null && !tagIds.isEmpty())
421        {
422            List<Expression> orExpr = new ArrayList<>();
423            for (String tagId : tagIds)
424            {
425                orExpr.add(new StringExpression(JCRCalendarEvent.METADATA_KEYWORDS, Operator.EQ, tagId));
426            }
427            keywordsExpr = new OrExpression(orExpr.toArray(new Expression[orExpr.size()]));
428        }
429        
430        // Get the non recurrent events sorted by ascending date and within the configured range
431        Expression eventExpr = new AndExpression(nonRecurrentExpr, startDateExpr, endDateExpr, keywordsExpr);
432        SortCriteria sortCriteria = new SortCriteria();
433        sortCriteria.addCriterion(JCRCalendarEvent.METADATA_START_DATE, true, false);
434        
435        String basicEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
436        AmetysObjectIterable<CalendarEvent> basicEvents = _resolver.query(basicEventQuery);
437        AmetysObjectIterator<CalendarEvent> basicEventIt = basicEvents.iterator();
438        
439        int processed = 0;
440        while (basicEventIt.hasNext() && processed < maxResults)
441        {
442            CalendarEvent event = basicEventIt.next();
443            Calendar holdingCalendar = (Calendar) event.getParent();
444            
445            if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
446            {
447                // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
448                
449                // FIXME should use something like an EventInfo object with some data + calendar, project name
450                // And use a function to process the transformation...
451                // Function<EventInfo, Map<String, Object>> fn. eventData.putAll(fn.apply(info));
452                
453                // standard set of event data
454                Map<String, Object> eventData = _calendarDAO.getEventData(event, false);
455                basicEventList.add(eventData);
456                processed++;
457                
458                // add additional info
459                _addAdditionalEventData(event, eventData);
460            }
461        }
462        
463        Expression recurrentExpr = new StringExpression(JCRCalendarEvent.METADATA_RECURRENCE_TYPE, Operator.NE, "NEVER");
464        eventExpr = new AndExpression(recurrentExpr, keywordsExpr);
465        
466        String recurrentEventQuery = QueryHelper.getXPathQuery(null, JCRCalendarEventFactory.CALENDAR_EVENT_NODETYPE, eventExpr, sortCriteria);
467        AmetysObjectIterable<CalendarEvent> recurrentEvents = _resolver.query(recurrentEventQuery);
468        AmetysObjectIterator<CalendarEvent> recurrentEventIt = recurrentEvents.iterator();
469        
470        Date nextDate = null;
471        // FIXME cannot count processed here...
472        processed = 0;
473        while (recurrentEventIt.hasNext() /*&& processed < maxResultsAsInt*/)
474        {
475            CalendarEvent event = recurrentEventIt.next();
476            nextDate = event.getNextOccurrence(startDate);
477            
478            // The recurrent event first occurrence is within the range
479            if (nextDate.before(endDate))
480            {
481                // FIXME calculate occurrences only if keep event...
482                List<Date> occurrences = event.getOccurrences(startDate, endDate);
483                Calendar holdingCalendar = (Calendar) event.getParent();
484                
485                if (_filterEvent(calendarIds, event) && _hasAccess(holdingCalendar))
486                {
487                    // The event is in the list of selected calendars and has the appropriate tags (can be none if tagIds == null)
488                    
489                    // Add all its occurrences that are within the range
490                    for (Date occurrence : occurrences)
491                    {
492                        Map<String, Object> eventData = _calendarDAO.getEventData(event, occurrence, false);
493                        recurrentEventList.add(eventData);
494                        processed++;
495                        
496                        _addAdditionalEventData(event, eventData);
497                        
498                    }
499                }
500            }
501        }
502        
503        // Re-sort chronologically the events' union
504        List<Map<String, Object>> allEvents = ListUtils.union(basicEventList, recurrentEventList);
505        Collections.sort(allEvents, new StartDateComparator());
506
507        // Return the first maxResults events
508        return allEvents.size() <= maxResults ? allEvents : allEvents.subList(0, maxResults);
509    }
510    
511    /**
512     * Determine whether the given event has to be kept or not depending on the given calendars
513     * @param calendarIds the ids of the calendars
514     * @param event the event
515     * @return true if the event can be kept, false otherwise
516     */
517    private boolean _filterEvent(List<String> calendarIds, CalendarEvent event)
518    {
519        Calendar holdingCalendar = (Calendar) event.getParent();
520        // FIXME calendarIds.get(0) == null means "All calendars" selected in the select calendar widget ??
521        // need cleaner code
522        return calendarIds == null || calendarIds.get(0) == null || calendarIds.contains(holdingCalendar.getId());
523    }
524    
525    private boolean _hasAccess(Calendar calendar)
526    {
527        return CalendarVisibility.PUBLIC.equals(calendar.getVisibility()) || canView(calendar);
528    }
529    
530    /**
531     * Get the data of every available calendar of the application
532     * @return the list of calendar data
533     */
534    @Callable
535    public List<Map<String, Object>> getCalendarsData()
536    {
537        List<Map<String, Object>> calendarsData = new ArrayList<>();
538        
539        AmetysObjectIterable<Project> projects = _projectManager.getProjects();
540        for (Project project : projects)
541        {
542            AmetysObjectIterable<Calendar> calendars = getCalendars(project);
543            if (calendars != null)
544            {
545                for (Calendar calendar : calendars)
546                {
547                    if (canView(calendar))
548                    {
549                        calendarsData.add(_calendarDAO.getCalendarData(calendar, false, false));
550                    }
551                }
552            }
553        }
554        
555        return calendarsData;
556    }
557    
558    /**
559     * Indicates if the current user can view the calendar
560     * @param calendar The calendar to test
561     * @return true if the calendar can be viewed 
562     */
563    public boolean canView(Calendar calendar)
564    {
565        return _projectRightHelper.hasReadAccess(calendar);
566    }
567    
568    /**
569     * Indicates if the current user can view the event
570     * @param event The event to test
571     * @return true if the event can be viewed 
572     */
573    public boolean canView(CalendarEvent event)
574    {
575        return _projectRightHelper.hasReadAccess(event.<ExplorerNode>getParent());
576    }
577    
578    
579    /**
580     * Compares events on their starting date
581     */
582    protected static class StartDateComparator implements Comparator<Map<String, Object>>
583    {
584        @Override
585        public int compare(Map<String, Object> calendarEventInfo1, Map<String, Object> calendarEventInfo2)
586        {
587            String startDate1asString = (String) calendarEventInfo1.get("startDate");
588            String startDate2asString = (String) calendarEventInfo2.get("startDate");
589            
590            Date startDate1 = DateUtils.parse(startDate1asString);
591            Date startDate2 = DateUtils.parse(startDate2asString);
592            
593            // The start date is before if 
594            return startDate1.compareTo(startDate2);
595        }
596    }
597    
598    @Override
599    public Set<String> getAllowedEventTypes()
600    {
601        return ImmutableSet.of("calendar.event.created", "calendar.event.updated");
602    }
603
604}
605