001/*
002 *  Copyright 2022 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 */
016
017package org.ametys.plugins.workspaces.calendars.events;
018
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027import java.util.stream.Stream;
028
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.commons.lang3.StringUtils;
032
033import org.ametys.core.right.RightManager.RightResult;
034import org.ametys.core.user.User;
035import org.ametys.core.user.UserIdentity;
036import org.ametys.core.util.DateUtils;
037import org.ametys.plugins.explorer.ExplorerNode;
038import org.ametys.plugins.workspaces.calendars.AbstractCalendarDAO;
039import org.ametys.plugins.workspaces.calendars.Calendar;
040import org.ametys.plugins.workspaces.calendars.CalendarDAO;
041import org.ametys.plugins.workspaces.calendars.task.TaskCalendarEvent;
042import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
043import org.ametys.plugins.workspaces.project.objects.Project;
044import org.ametys.plugins.workspaces.tasks.Task;
045import org.ametys.plugins.workspaces.tasks.TasksWorkspaceModule;
046
047/**
048 * Helper to convert events to JSON
049 */
050public class CalendarEventJSONHelper extends AbstractCalendarDAO
051{
052    /** Avalon Role */
053    public static final String ROLE = CalendarEventJSONHelper.class.getName();
054
055    private static final String __HOUR_PATTERN = "yyyyMMdd'T'HHmmssXXX";
056    
057//    private static final String __FULL_DAY_PATTERN = "uuuuMMdd";
058    
059    /** Calendar DAO */
060    protected CalendarDAO _calendarDAO;
061
062    private TasksWorkspaceModule _taskModule;
063    
064    @Override
065    public void service(ServiceManager manager) throws ServiceException
066    {
067        super.service(manager);
068        _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE);
069        WorkspaceModuleExtensionPoint moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
070        _taskModule = moduleManagerEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID);
071    }
072        
073    /**
074     * Get event info for a specific occurrence
075     * @param event The event
076     * @param occurrenceDate the occurrence
077     * @param fullInfo true to include full info (rights, parent id, etc...)
078     * @return the event data in a map
079     */
080    public Map<String, Object> eventAsJsonWithOccurrence(CalendarEvent event, ZonedDateTime occurrenceDate, boolean fullInfo)
081    {
082        Map<String, Object> eventData = eventAsJson(event, fullInfo, false);
083        
084        Optional<CalendarEventOccurrence> optionalEvent = event.getFirstOccurrence(occurrenceDate);
085        if (optionalEvent.isPresent())
086        {
087            eventData.putAll(optionalEvent.get().toJSON());
088        }
089        
090        return eventData;
091    }
092    
093    /**
094     * Get event info
095     * @param event The event
096     * @param fullInfo true to include full info (rights, parent id, etc...)
097     * @param startDate The start date.
098     * @param endDate The end date.
099     * @return the event data in a map
100     */
101    public Map<String, Object> eventAsJsonWithOccurrences(CalendarEvent event, boolean fullInfo, ZonedDateTime startDate, ZonedDateTime endDate)
102    {
103        Map<String, Object> eventData = eventAsJson(event, false, false);
104
105        List<CalendarEventOccurrence> occurences = event.getOccurrences(startDate, endDate);
106        
107        List<Object> occurrencesDataList = new ArrayList<>();
108        eventData.put("occurrences", occurrencesDataList);
109        
110        for (CalendarEventOccurrence occurence : occurences)
111        {
112            occurrencesDataList.add(occurence.toJSON());
113        }
114        return eventData;
115    }
116    
117    /**
118     * Get event info
119     * @param event The event
120     * @param fullInfo true to include full info (rights, parent id, etc...)
121     * @param useICSFormat true to use ICS Format for dates
122     * @return the event data in a map
123     */
124    public Map<String, Object> eventAsJson(CalendarEvent event, boolean fullInfo, boolean useICSFormat)
125    {
126        
127        Calendar calendar = event.getCalendar();
128        Map<String, Object> result = new HashMap<>();
129        
130        result.put("id", event.getId());
131        result.put("calendarId", calendar.getId());
132        result.put("color", calendar.getColor());
133        
134        result.put("title", event.getTitle());
135        result.put("description", event.getDescription());
136        
137        boolean fullDay = event.getFullDay();
138        
139        result.put("fullDay", fullDay);
140        result.put("recurrenceType", event.getRecurrenceType().toString());
141        
142        result.put("location", event.getLocation());
143        result.put("keywords", event.getTags());
144        
145        ZonedDateTime untilDate = event.getRepeatUntil();
146        if (untilDate != null)
147        {
148            if (useICSFormat)
149            {
150                // iCalendar/ICS handle until date slightly differently, so we have to add one day to include the last occurrence
151                untilDate = untilDate.plusDays(1).minusSeconds(1);
152            }
153            
154            result.put("untilDate", formatDate(untilDate, useICSFormat, fullDay));
155        }
156        
157        ZonedDateTime startDateEvent = event.getStartDate();
158        ZonedDateTime endDateEvent = event.getEndDate();
159        
160        if (event.getFullDay())
161        {
162            result.put("endDateNextDay", formatDate(endDateEvent.plusDays(1), useICSFormat, fullDay));
163        }
164
165        result.put("startDate", formatDate(startDateEvent, useICSFormat, fullDay));
166        
167        if (event.getFullDay() && useICSFormat)
168        {
169            result.put("endDate", formatDate(endDateEvent.plusDays(1), useICSFormat, fullDay));
170        }
171        else
172        {
173            result.put("endDate", formatDate(endDateEvent, useICSFormat, fullDay));
174        }
175        
176        //excluded occurences
177        List<ZonedDateTime> excludedOccurences = event.getExcludedOccurences();
178        if (excludedOccurences != null && !excludedOccurences.isEmpty())
179        {
180            List<String> excludedOccurencesStrings = new ArrayList<>();
181            for (ZonedDateTime excludedOccurence : excludedOccurences)
182            {
183                excludedOccurencesStrings.add(formatDate(excludedOccurence, useICSFormat, true));
184            }
185            result.put("excludedDates", excludedOccurencesStrings);
186        }
187
188        // creator
189        UserIdentity creatorIdentity = event.getCreator();
190        User creator = _userManager.getUser(creatorIdentity);
191        
192        result.put("creator", creatorIdentity);
193        result.put("creatorFullName", creator != null ? creator.getFullName() : creatorIdentity.getLogin());
194
195        UserIdentity user = _currentUserProvider.getUser();
196        result.put("isCreator", creatorIdentity.equals(user));
197        result.put("creationDate", formatDate(event.getCreationDate(), useICSFormat, false));
198        
199        // last modification
200        UserIdentity contributorIdentity = event.getLastContributor();
201        User contributor = _userManager.getUser(contributorIdentity);
202        
203        result.put("contributor", contributorIdentity);
204        result.put("contributorFullName", contributor != null ? contributor.getFullName() : contributorIdentity.getLogin());
205        result.put("lastModified", formatDate(event.getLastModified(), useICSFormat, false));
206        
207        if (fullInfo)
208        {
209            result.putAll(_eventAsJsonFullInfo(event));
210        }
211
212        result.put("calendar", _calendarDAO.getCalendarProperties(calendar));
213        
214        // tags and places are expected by the client (respectively keywords and location on the server side)
215        result.put("tags", event.getTags());
216        
217        String location = StringUtils.defaultString(event.getLocation());
218        result.put("location", location);
219        result.put("places", Stream.of(location.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList()));
220        
221        // add event rights
222        result.put("rights", _extractEventRightData(event));
223        
224        result.put("resourceIds", event.getResources());
225        
226        result.put("isModifiable", event instanceof ModifiableCalendarEvent);
227        if (event instanceof TaskCalendarEvent taskCalendarEvent)
228        {
229            Task task = taskCalendarEvent.getTask();
230            Project project = _projectManager.getParentProject(task);
231            result.put("taskURL",  _taskModule.getTaskUri(project, task.getId()));
232           
233        }
234
235        return result;
236    }
237    
238    private String formatDate(ZonedDateTime date, boolean useICSFormat, boolean fullDay)
239    {
240        if (useICSFormat)
241        {
242            if (fullDay)
243            {
244                // FIXME : commented because currently, dates are badly stored, waiting for a data migration to use it back
245            //  return DateUtils.zonedDateTimeToString(date, date.getZone(), __FULL_DAY_PATTERN);
246                return DateUtils.zonedDateTimeToString(date);
247            }
248            else
249            {
250                return DateUtils.zonedDateTimeToString(date, date.getZone(), __HOUR_PATTERN);
251            }
252        }
253        else
254        {
255            return DateUtils.zonedDateTimeToString(date);
256        }
257    }
258    
259    /**
260     * Retrieves the event additional info (rights, parent id, etc...)
261     * @param event The event
262     * @return the event additional info (rights, parent id, etc...) in a map
263     */
264    protected Map<String, Object> _eventAsJsonFullInfo(CalendarEvent event)
265    {
266        Map<String, Object> result = new HashMap<>();
267        
268        ExplorerNode explorerNode = event.getParent();
269        ExplorerNode root = explorerNode;
270        while (true)
271        {
272            if (root.getParent() instanceof ExplorerNode)
273            {
274                root = root.getParent();
275            }
276            else
277            {
278                break;
279            }
280        }
281        result.put("rootId", root.getId());
282        result.put("parentId", explorerNode.getId());
283        result.put("name", event.getName());
284        result.put("path", explorerNode.getExplorerPath());
285        
286        result.put("rights", _getUserRights(explorerNode));
287        
288        return result;
289    }
290    
291    /**
292     * Internal method to extract the data concerning the right of the current user for an event
293     * @param event The event
294     * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not.
295     */
296    protected  Map<String, Object> _extractEventRightData(CalendarEvent event)
297    {
298        Map<String, Object> rightsData = new HashMap<>();
299        UserIdentity user = _currentUserProvider.getUser();
300        Calendar calendar = event.getCalendar();
301        
302        rightsData.put("edit", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_EDIT, calendar) == RightResult.RIGHT_ALLOW);
303        rightsData.put("delete", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_DELETE, calendar) == RightResult.RIGHT_ALLOW);
304        rightsData.put("delete-own", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW);
305        
306        return rightsData;
307    }
308    
309    /**
310     * Get the user rights on the resource collection
311     * @param node The explorer node
312     * @return The user's rights
313     */
314    protected Set<String> _getUserRights(ExplorerNode node)
315    {
316        return _rightManager.getUserRights(_currentUserProvider.getUser(), node);
317    }
318    
319}