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