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 */ 016package org.ametys.plugins.calendar.icsreader; 017 018import java.time.Instant; 019import java.time.ZoneOffset; 020import java.time.ZonedDateTime; 021import java.time.temporal.Temporal; 022import java.util.ArrayList; 023import java.util.HashSet; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.UUID; 029import java.util.stream.Collectors; 030 031import org.apache.avalon.framework.component.Component; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.avalon.framework.service.Serviceable; 035import org.apache.cocoon.xml.AttributesImpl; 036import org.apache.cocoon.xml.XMLUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.commons.lang3.tuple.ImmutablePair; 039import org.apache.commons.lang3.tuple.Pair; 040import org.xml.sax.ContentHandler; 041import org.xml.sax.SAXException; 042 043import org.ametys.cms.tag.Tag; 044import org.ametys.cms.tag.TagProviderExtensionPoint; 045import org.ametys.core.util.DateUtils; 046import org.ametys.plugins.calendar.events.EventsFilterHelper; 047import org.ametys.plugins.calendar.icsreader.IcsReader.IcsEvents; 048import org.ametys.plugins.calendar.search.CalendarSearchService; 049import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater; 050import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry; 051import org.ametys.web.repository.page.ZoneItem; 052 053import net.fortuna.ical4j.model.Parameter; 054import net.fortuna.ical4j.model.Property; 055import net.fortuna.ical4j.model.component.VEvent; 056import net.fortuna.ical4j.model.parameter.TzId; 057import net.fortuna.ical4j.model.property.Created; 058import net.fortuna.ical4j.model.property.DtEnd; 059import net.fortuna.ical4j.model.property.DtStamp; 060import net.fortuna.ical4j.model.property.DtStart; 061import net.fortuna.ical4j.model.property.LastModified; 062import net.fortuna.ical4j.model.property.Url; 063 064/** 065 * Helper from ICS events 066 */ 067public class IcsEventHelper implements Component, Serviceable 068{ 069 /** The component role. */ 070 public static final String ROLE = IcsEventHelper.class.getName(); 071 072 /** The ICS reader */ 073 protected IcsReader _icsReader; 074 /** The tag provider extention point */ 075 protected TagProviderExtensionPoint _tagProviderEP; 076 077 public void service(ServiceManager smanager) throws ServiceException 078 { 079 _icsReader = (IcsReader) smanager.lookup(IcsReader.ROLE); 080 _tagProviderEP = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 081 } 082 083 /** 084 * Read the configured distant ics sources into a list of {@link IcsEvents} 085 * @param zoneItem zoneItem where the configuration will be fetched 086 * @param siteName name of the current site 087 * @param dateRange range of dates to limit 088 * @return a list of {@link IcsEvents} 089 */ 090 public List<IcsEvents> getICSEvents(ZoneItem zoneItem, String siteName, EventsFilterHelper.DateTimeRange dateRange) 091 { 092 List<IcsEvents> icsEvents = new ArrayList<>(); 093 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics")) 094 { 095 Long nbIcsEvent = zoneItem.getServiceParameters().getValue("nbEvents"); 096 Long maxIcsSize = zoneItem.getServiceParameters().getValue("maxSize"); 097 098 icsEvents = getICSEvents(zoneItem, siteName, dateRange, nbIcsEvent, maxIcsSize); 099 } 100 return icsEvents; 101 } 102 103 /** 104 * Read the configured distant ics sources into a list of {@link IcsEvents} 105 * @param zoneItem zoneItem where the configuration will be fetched 106 * @param siteName name of the current site 107 * @param dateRange range of dates to limit 108 * @param nbEvents number of events to read 109 * @param maxFileSize max ics file size (in bytes) 110 * @return a list of {@link IcsEvents} 111 */ 112 public List<IcsEvents> getICSEvents(ZoneItem zoneItem, String siteName, EventsFilterHelper.DateTimeRange dateRange, Long nbEvents, Long maxFileSize) 113 { 114 List<IcsEvents> icsEvents = new ArrayList<>(); 115 116 if (zoneItem.getServiceParameters().hasValue("ics")) 117 { 118 ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics"); 119 120 for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries()) 121 { 122 String url = repeaterEntry.getValue("url"); 123 IcsEvents eventList = _icsReader.getEventList(url, dateRange, nbEvents, maxFileSize); 124 125 String tagName = repeaterEntry.getValue("tag"); 126 if (StringUtils.isNotEmpty(tagName)) 127 { 128 Tag tag = _tagProviderEP.getTag(tagName, Map.of("siteName", siteName)); 129 if (tag != null) 130 { 131 eventList.setTag(tag); 132 } 133 } 134 135 icsEvents.add(eventList); 136 } 137 } 138 139 return icsEvents; 140 } 141 142 /** 143 * Get the ICS tags from search service 144 * @param zoneItem The zone item id 145 * @param siteName the site name 146 * @return the ICS tags 147 */ 148 public Set<Tag> getIcsTags(ZoneItem zoneItem, String siteName) 149 { 150 Set<Tag> tags = new LinkedHashSet<>(); 151 152 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics")) 153 { 154 ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics"); 155 for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries()) 156 { 157 String tagName = repeaterEntry.getValue("tag"); 158 Tag tag = _tagProviderEP.getTag(tagName, Map.of("siteName", siteName)); 159 if (tag != null) 160 { 161 tags.add(tag); 162 } 163 } 164 } 165 166 return tags; 167 } 168 169 /** 170 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 171 * @param icsEventsList the list of {@link IcsEvents} 172 * @param dateRange range of dates to limit 173 * @return a list of {@link LocalVEvent} 174 */ 175 public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange) 176 { 177 return toLocalIcsEvent(icsEventsList, dateRange, List.of()); 178 } 179 180 /** 181 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 182 * @param icsEventsList the list of {@link IcsEvents} 183 * @param dateRange range of dates to limit 184 * @param filteredTags A list of tag's name to filter ICS events. Can be empty to no filter on tags. 185 * @return a list of {@link LocalVEvent} 186 */ 187 public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange, List<String> filteredTags) 188 { 189 List<LocalVEvent> localICSEvents = new ArrayList<>(); 190 String fullICSDistantEvents = ""; 191 for (IcsEvents icsEvents : icsEventsList) 192 { 193 if (icsEvents.hasEvents()) 194 { 195 Tag tag = icsEvents.getTag(); 196 if (filteredTags.isEmpty() || tag != null && filteredTags.contains(tag.getName())) 197 { 198 for (VEvent calendarComponent : icsEvents.getEvents()) 199 { 200 List<LocalVEvent> localCalendarComponents = _icsReader.getEventDates(calendarComponent, dateRange, tag); 201 localICSEvents.addAll(localCalendarComponents); 202 if (dateRange == null) 203 { 204 // To avoid to have the ICS events in double in some exotic non-anticipated cases, when there is a dateRange, we do not copy the ICS 205 fullICSDistantEvents += calendarComponent.toString() + "\n"; 206 } 207 } 208 } 209 } 210 } 211 212 return new ImmutablePair<>(localICSEvents, fullICSDistantEvents); 213 } 214 /** 215 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 216 * @param icsEventsList the list of {@link IcsEvents} 217 * @param dateRange range of dates to limit 218 * @param filteredTags A list of tag's name to filter ICS events. Can be empty to no filter on tags. 219 * @return a list of {@link LocalVEvent} 220 */ 221 public String toVTimeZone(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange, List<String> filteredTags) 222 { 223 Set<String> timeZoneIds = new HashSet<>(); 224 String timeZonesAsString = ""; 225 for (IcsEvents icsEvents : icsEventsList) 226 { 227 if (icsEvents.hasEvents()) 228 { 229 Tag tag = icsEvents.getTag(); 230 if (filteredTags.isEmpty() || tag != null && filteredTags.contains(tag.getName())) 231 { 232 for (VEvent calendarComponent : icsEvents.getEvents()) 233 { 234 DtStart<Temporal> dateTimeStart = calendarComponent.getDateTimeStart(); 235 TzId startDateTZ = dateTimeStart != null ? dateTimeStart.<TzId>getParameter(Parameter.TZID).orElse(null) : null; 236 if (startDateTZ != null) 237 { 238 if (!timeZoneIds.contains(startDateTZ.getValue())) 239 { 240 timeZoneIds.add(startDateTZ.getValue()); 241 242 timeZonesAsString += startDateTZ.getValue() + "\n"; 243 } 244 } 245 DtEnd<Temporal> dateTimeEnd = calendarComponent.getDateTimeEnd(); 246 TzId endDateTZ = dateTimeEnd != null ? dateTimeEnd.<TzId>getParameter(Parameter.TZID).orElse(null) : null; 247 if (endDateTZ != null) 248 { 249 if (!timeZoneIds.contains(endDateTZ.getValue())) 250 { 251 timeZoneIds.add(endDateTZ.getValue()); 252 253 timeZonesAsString += endDateTZ.getValue() + "\n"; 254 } 255 } 256 } 257 } 258 } 259 } 260 261 return timeZonesAsString; 262 } 263 264 /** 265 * SAX ics events hits 266 * @param handler the content handler 267 * @param icsEvents The ics events 268 * @param startNumber the start index 269 * @throws SAXException if an error occurred while saxing 270 */ 271 public void saxIcsEventHits(ContentHandler handler, List<LocalVEvent> icsEvents, int startNumber) throws SAXException 272 { 273 int hitIndex = startNumber; 274 for (LocalVEvent icsEvent : icsEvents) 275 { 276 saxIcsEventHit(handler, icsEvent, hitIndex++); 277 } 278 } 279 280 /** 281 * SAX a ics events hit 282 * @param handler the content handler 283 * @param icsEvent The ics event 284 * @param number the hit index 285 * @throws SAXException if an error occurred while saxing 286 */ 287 public void saxIcsEventHit(ContentHandler handler, LocalVEvent icsEvent, int number) throws SAXException 288 { 289 VEvent event = icsEvent.getEvent(); 290 291 AttributesImpl attrs = new AttributesImpl(); 292 attrs.addCDATAAttribute("number", Integer.toString(number)); 293 attrs.addCDATAAttribute("icsEvent", "true"); 294 XMLUtils.startElement(handler, "hit", attrs); 295 296 String id = event.getProperty(Property.UID).map(Property::getValue).orElse(UUID.randomUUID().toString()); 297 XMLUtils.createElement(handler, "id", id); 298 299 saxIcsEvent(handler, icsEvent); 300 301 XMLUtils.endElement(handler, "hit"); 302 } 303 304 /** 305 * SAX a ics event 306 * @param handler the content handler 307 * @param icsEvent The ics event 308 * @throws SAXException if an error occurred while saxing 309 */ 310 public void saxIcsEvent(ContentHandler handler, LocalVEvent icsEvent) throws SAXException 311 { 312 XMLUtils.startElement(handler, "event"); 313 314 VEvent event = icsEvent.getEvent(); 315 316 String title = event.getProperty(Property.SUMMARY).map(Property::getValue).orElse(StringUtils.EMPTY); 317 XMLUtils.createElement(handler, "title", title); 318 319 String description = event.getProperty(Property.DESCRIPTION).map(Property::getValue).orElse(StringUtils.EMPTY); 320 XMLUtils.createElement(handler, "description", description); 321 322 DtStamp dateTimeStamp = event.getDateTimeStamp(); 323 Instant dtStamp = dateTimeStamp != null ? dateTimeStamp.getDate() : Instant.now(); 324 325 Created created = event.getCreated(); 326 Instant creationDate = created != null ? created.getDate() : dtStamp; 327 328 LastModified lastModified = event.getLastModified(); 329 Instant lastModifiedDate = lastModified != null ? lastModified.getDate() : dtStamp; 330 331 _saxInstant(handler, "creationDate", creationDate); 332 _saxInstant(handler, "lastModifiedDate", lastModifiedDate); 333 334 _saxZonedDateTime(handler, "startDate", icsEvent.getStart()); 335 _saxZonedDateTime(handler, "endDate", icsEvent.getEnd()); 336 337 _saxAllDay(handler, icsEvent); 338 339 CalendarSearchService.saxTag(handler, icsEvent.getTag()); 340 341 Url url = event.getUrl(); 342 if (url != null) 343 { 344 XMLUtils.createElement(handler, "url", url.getValue()); 345 } 346 347 XMLUtils.endElement(handler, "event"); 348 349 } 350 351 /** 352 * Sax all day property 353 * @param handler the content handler 354 * @param icsEvent the ICS event 355 * @throws SAXException if an error occurred while saxing 356 */ 357 protected void _saxAllDay(ContentHandler handler, LocalVEvent icsEvent) throws SAXException 358 { 359 VEvent event = icsEvent.getEvent(); 360 361 DtStart startDate = event.getDateTimeStart(); 362 DtEnd endDate = event.getDateTimeEnd(); 363 364 boolean allDayEvent = startDate != null && "DATE".equals(startDate.getParameter("VALUE").map(Parameter::getValue).orElse(null)) 365 && (endDate == null || "DATE".equals(endDate.getParameter("VALUE").map(Parameter::getValue).orElse(null))); 366 367 XMLUtils.createElement(handler, "allDay", String.valueOf(allDayEvent)); 368 } 369 370 /** 371 * Sax ICS date 372 * @param handler the content handler 373 * @param tagName the xml tag name 374 * @param zonedDatetime the ZonedDateTime to sax 375 * @throws SAXException if an error occurred while saxing 376 */ 377 protected void _saxZonedDateTime(ContentHandler handler, String tagName, ZonedDateTime zonedDatetime) throws SAXException 378 { 379 if (zonedDatetime != null) 380 { 381 XMLUtils.createElement(handler, tagName, DateUtils.getISODateTimeFormatter().format(zonedDatetime)); 382 } 383 } 384 385 /** 386 * Sax ICS date 387 * @param handler the content handler 388 * @param tagName the xml tag name 389 * @param instant the Instant to sax 390 * @throws SAXException if an error occurred while saxing 391 */ 392 protected void _saxInstant(ContentHandler handler, String tagName, Instant instant) throws SAXException 393 { 394 if (instant != null) 395 { 396 _saxZonedDateTime(handler, tagName, instant.atZone(ZoneOffset.UTC)); 397 } 398 } 399 400 /** 401 * Sax ICS errors 402 * @param icsResults the ICS events 403 * @param handler the content handler 404 * @throws SAXException if an error occurred 405 */ 406 public void saxICSErrors(List<IcsEvents> icsResults, ContentHandler handler) throws SAXException 407 { 408 List<IcsEvents> errorsIcs = icsResults.stream() 409 .filter(ics -> ics.getStatus() != IcsEvents.Status.OK) 410 .collect(Collectors.toList()); 411 412 if (!errorsIcs.isEmpty()) 413 { 414 XMLUtils.startElement(handler, "errors-ics"); 415 for (IcsEvents ics : errorsIcs) 416 { 417 AttributesImpl attrs = new AttributesImpl(); 418 attrs.addCDATAAttribute("url", ics.getUrl()); 419 attrs.addCDATAAttribute("status", ics.getStatus().name()); 420 XMLUtils.createElement(handler, "ics", attrs); 421 } 422 XMLUtils.endElement(handler, "errors-ics"); 423 } 424 } 425}