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