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