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.LocalDate; 020import java.time.ZoneId; 021import java.time.ZoneOffset; 022import java.time.ZonedDateTime; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.Set; 029import java.util.stream.Collectors; 030import java.util.stream.Stream; 031 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.core.right.RightManager.RightResult; 037import org.ametys.core.user.User; 038import org.ametys.core.user.UserIdentity; 039import org.ametys.core.util.DateUtils; 040import org.ametys.plugins.explorer.ExplorerNode; 041import org.ametys.plugins.workspaces.calendars.AbstractCalendarDAO; 042import org.ametys.plugins.workspaces.calendars.Calendar; 043import org.ametys.plugins.workspaces.calendars.CalendarDAO; 044import org.ametys.plugins.workspaces.calendars.task.TaskCalendarEvent; 045import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 046import org.ametys.plugins.workspaces.project.objects.Project; 047import org.ametys.plugins.workspaces.tasks.Task; 048import org.ametys.plugins.workspaces.tasks.TasksWorkspaceModule; 049 050/** 051 * Helper to convert events to JSON 052 */ 053public class CalendarEventJSONHelper extends AbstractCalendarDAO 054{ 055 /** Avalon Role */ 056 public static final String ROLE = CalendarEventJSONHelper.class.getName(); 057 058 private static final String __HOUR_PATTERN_LOCAL = "yyyyMMdd'T'HHmmss"; 059 private static final String __HOUR_PATTERN_UTC = "yyyyMMdd'T'HHmmss'Z'"; 060 private static final String __FULL_DAY_PATTERN = "uuuuMMdd"; 061 062 /** Calendar DAO */ 063 protected CalendarDAO _calendarDAO; 064 065 private TasksWorkspaceModule _taskModule; 066 067 @Override 068 public void service(ServiceManager manager) throws ServiceException 069 { 070 super.service(manager); 071 _calendarDAO = (CalendarDAO) manager.lookup(CalendarDAO.ROLE); 072 WorkspaceModuleExtensionPoint moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 073 _taskModule = moduleManagerEP.getModule(TasksWorkspaceModule.TASK_MODULE_ID); 074 } 075 076 /** 077 * Get event info for a specific occurrence 078 * @param event The event 079 * @param occurrenceDate the occurrence 080 * @return the event data in a map 081 */ 082 public Map<String, Object> eventAsJsonWithOccurrence(CalendarEvent event, ZonedDateTime occurrenceDate) 083 { 084 Map<String, Object> eventData = eventAsJson(event, false); 085 086 Optional<CalendarEventOccurrence> optionalEvent = event.getFirstOccurrence(occurrenceDate); 087 if (optionalEvent.isPresent()) 088 { 089 eventData.putAll(optionalEvent.get().toJSON()); 090 } 091 092 return eventData; 093 } 094 095 /** 096 * Get event info 097 * @param event The event 098 * @param startDate The start date. 099 * @param endDate The end date. 100 * @return the event data in a map 101 */ 102 public Map<String, Object> eventAsJsonWithOccurrences(CalendarEvent event, ZonedDateTime startDate, ZonedDateTime endDate) 103 { 104 Map<String, Object> eventData = eventAsJson(event, false); 105 106 List<CalendarEventOccurrence> occurences = event.getOccurrences(startDate, endDate); 107 108 List<Object> occurrencesDataList = new ArrayList<>(); 109 eventData.put("occurrences", occurrencesDataList); 110 111 for (CalendarEventOccurrence occurence : occurences) 112 { 113 occurrencesDataList.add(occurence.toJSON()); 114 } 115 return eventData; 116 } 117 118 /** 119 * Get event info 120 * @param event The event 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 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 146 ZoneId eventZone = event.getZone(); 147 ZonedDateTime startDate = event.getStartDate(); 148 ZonedDateTime endDate = event.getEndDate(); 149 150 ZonedDateTime untilDate = event.getRepeatUntil(); 151 if (untilDate != null) 152 { 153 result.put("untilDate", formatDate(untilDate, useICSFormat, fullDay, eventZone, true)); 154 } 155 156 // the list of excluded event date as ZonedDateTime at midnight in event timezone. 157 List<ZonedDateTime> excludedOccurrences = event.getExcludedOccurences(); 158 if (excludedOccurrences != null && !excludedOccurrences.isEmpty()) 159 { 160 161 if (useICSFormat) 162 { 163 List<String> excludedStrings = new ArrayList<>(); 164 for (ZonedDateTime excluded : excludedOccurrences) 165 { 166 if (!fullDay) 167 { 168 // Some calendar systems need EXDATE in DATE-TIME and not only DATE, however we only store excluded dates as full days, even if the event is not a full day event. 169 // The stored value is a ZonedDateTime at midnight in event timezone, that we retrieve as UTC converted ZonedDateTime 170 171 // Get the date of the excluded occurrence in the event time zone, to avoid issues with daylight saving time change or other edge cases. 172 LocalDate excludedDateInEventZone = excluded.withZoneSameInstant(eventZone).toLocalDate(); 173 174 // Apply the excluded date to the start date and hour of the event, to get the correct time during the excluded occurrence. 175 ZonedDateTime excludedWithHour = startDate.withZoneSameInstant(eventZone).with(excludedDateInEventZone); 176 177 excludedStrings.add(formatDate(excludedWithHour, true, false, eventZone)); 178 } 179 else 180 { 181 excludedStrings.add(formatDate(excluded, true, true, eventZone)); 182 } 183 } 184 result.put("excludedDates", excludedStrings); 185 } 186 else 187 { 188 result.put("excludedDates", excludedOccurrences.stream() 189 .map(DateUtils::zonedDateTimeToString) 190 .collect(Collectors.toList())); 191 } 192 193 } 194 195 if (!fullDay) 196 { 197 result.put("zone", eventZone.getId()); 198 } 199 200 result.put("startDate", formatDate(startDate, useICSFormat, fullDay, eventZone)); 201 202 if (fullDay && useICSFormat) 203 { 204 // iCalendar full-day DTEND is exclusive, so add one day 205 result.put("endDate", formatDate(endDate.plusDays(1), true, fullDay, eventZone)); 206 } 207 else 208 { 209 result.put("endDate", formatDate(endDate, useICSFormat, fullDay, eventZone)); 210 } 211 212 // creator 213 UserIdentity creatorIdentity = event.getCreator(); 214 User creator = _userManager.getUser(creatorIdentity); 215 216 result.put("creator", creatorIdentity); 217 result.put("creatorFullName", creator != null ? creator.getFullName() : StringUtils.EMPTY); 218 219 UserIdentity user = _currentUserProvider.getUser(); 220 result.put("isCreator", creatorIdentity.equals(user)); 221 result.put("creationDate", formatDate(event.getCreationDate(), useICSFormat, false, null)); 222 223 // last modification 224 UserIdentity contributorIdentity = event.getLastContributor(); 225 User contributor = _userManager.getUser(contributorIdentity); 226 227 result.put("contributor", contributorIdentity); 228 result.put("contributorFullName", contributor != null ? contributor.getFullName() : contributorIdentity.getLogin()); 229 result.put("lastModified", formatDate(event.getLastModified(), useICSFormat, false, null)); 230 231 result.put("calendar", _calendarDAO.getCalendarProperties(calendar)); 232 233 // tags and places are expected by the client (respectively keywords and location on the server side) 234 result.put("tags", event.getTags()); 235 236 String location = StringUtils.defaultString(event.getLocation()); 237 result.put("location", location); 238 result.put("places", Stream.of(location.split(",")).filter(StringUtils::isNotEmpty).collect(Collectors.toList())); 239 240 // add event rights 241 result.put("rights", _extractEventRightData(event)); 242 243 result.put("resourceIds", event.getResources()); 244 245 result.put("isModifiable", event instanceof ModifiableCalendarEvent); 246 if (event instanceof TaskCalendarEvent taskCalendarEvent) 247 { 248 Task task = taskCalendarEvent.getTask(); 249 Project project = _projectManager.getParentProject(task); 250 result.put("taskURL", _taskModule.getTaskUri(project, task.getId())); 251 252 } 253 254 return result; 255 } 256 257 private String formatDate(ZonedDateTime date, boolean useICSFormat, boolean fullDay, ZoneId eventZone) 258 { 259 return formatDate(date, useICSFormat, fullDay, eventZone, false); 260 } 261 262 private String formatDate(ZonedDateTime date, boolean useICSFormat, boolean fullDay, ZoneId eventZone, boolean forceUTC) 263 { 264 if (useICSFormat) 265 { 266 if (fullDay) 267 { 268 // Convert ZonedDateTime to local date according to event time zone 269 return DateUtils.zonedDateTimeToString(date, eventZone, __FULL_DAY_PATTERN); 270 271 } 272 else if (eventZone != null && !forceUTC) 273 { 274 // Convert to event zone and format as local time (no offset) for TZID-based iCalendar properties 275 return DateUtils.zonedDateTimeToString(date, eventZone, __HOUR_PATTERN_LOCAL); 276 } 277 else 278 { 279 // Metadata dates (CREATED, LAST-MODIFIED, DTSTAMP) and UNTIL must be in UTC per RFC 5545 280 return DateUtils.zonedDateTimeToString(date, ZoneOffset.UTC, __HOUR_PATTERN_UTC); 281 } 282 } 283 else 284 { 285 return DateUtils.zonedDateTimeToString(date); 286 } 287 } 288 289 /** 290 * Internal method to extract the data concerning the right of the current user for an event 291 * @param event The event 292 * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not. 293 */ 294 protected Map<String, Object> _extractEventRightData(CalendarEvent event) 295 { 296 Map<String, Object> rightsData = new HashMap<>(); 297 UserIdentity user = _currentUserProvider.getUser(); 298 Calendar calendar = event.getCalendar(); 299 300 rightsData.put("edit", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_EDIT, calendar) == RightResult.RIGHT_ALLOW); 301 rightsData.put("delete", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_DELETE, calendar) == RightResult.RIGHT_ALLOW); 302 rightsData.put("delete-own", event instanceof ModifiableCalendarEvent && _rightManager.hasRight(user, AbstractCalendarDAO.RIGHTS_EVENT_DELETE_OWN, calendar) == RightResult.RIGHT_ALLOW); 303 304 return rightsData; 305 } 306 307 /** 308 * Get the user rights on the resource collection 309 * @param node The explorer node 310 * @return The user's rights 311 */ 312 protected Set<String> _getUserRights(ExplorerNode node) 313 { 314 return _rightManager.getUserRights(_currentUserProvider.getUser(), node); 315 } 316 317}