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 if (tag != null) 123 { 124 eventList.setTag(tag); 125 } 126 } 127 128 icsEvents.add(eventList); 129 } 130 } 131 132 return icsEvents; 133 } 134 135 /** 136 * Get the ICS tags from search service 137 * @param zoneItem The zone item id 138 * @param siteName the site name 139 * @return the ICS tags 140 */ 141 public Set<Tag> getIcsTags(ZoneItem zoneItem, String siteName) 142 { 143 Set<Tag> tags = new LinkedHashSet<>(); 144 145 if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics")) 146 { 147 ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics"); 148 for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries()) 149 { 150 String tagName = repeaterEntry.getValue("tag"); 151 Tag tag = _tagProviderEP.getTag(tagName, Map.of("siteName", siteName)); 152 if (tag != null) 153 { 154 tags.add(tag); 155 } 156 } 157 } 158 159 return tags; 160 } 161 162 /** 163 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 164 * @param icsEventsList the list of {@link IcsEvents} 165 * @param dateRange range of dates to limit 166 * @return a list of {@link LocalVEvent} 167 */ 168 public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange) 169 { 170 return toLocalIcsEvent(icsEventsList, dateRange, List.of()); 171 } 172 173 /** 174 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 175 * @param icsEventsList the list of {@link IcsEvents} 176 * @param dateRange range of dates to limit 177 * @param filteredTags A list of tag's name to filter ICS events. Can be empty to no filter on tags. 178 * @return a list of {@link LocalVEvent} 179 */ 180 public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange, List<String> filteredTags) 181 { 182 List<LocalVEvent> localICSEvents = new ArrayList<>(); 183 String fullICSDistantEvents = ""; 184 for (IcsEvents icsEvents : icsEventsList) 185 { 186 if (icsEvents.hasEvents()) 187 { 188 Tag tag = icsEvents.getTag(); 189 if (filteredTags.isEmpty() || tag != null && filteredTags.contains(tag.getName())) 190 { 191 for (VEvent calendarComponent : icsEvents.getEvents()) 192 { 193 List<LocalVEvent> localCalendarComponents = _icsReader.getEventDates(calendarComponent, dateRange, tag); 194 localICSEvents.addAll(localCalendarComponents); 195 if (dateRange == null) 196 { 197 // 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 198 fullICSDistantEvents += calendarComponent.toString() + "\n"; 199 } 200 } 201 } 202 } 203 } 204 205 return new ImmutablePair<>(localICSEvents, fullICSDistantEvents); 206 } 207 /** 208 * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents} 209 * @param icsEventsList the list of {@link IcsEvents} 210 * @param dateRange range of dates to limit 211 * @param filteredTags A list of tag's name to filter ICS events. Can be empty to no filter on tags. 212 * @return a list of {@link LocalVEvent} 213 */ 214 public String toVTimeZone(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange, List<String> filteredTags) 215 { 216 Set<String> timeZoneIds = new HashSet<>(); 217 String timeZonesAsString = ""; 218 for (IcsEvents icsEvents : icsEventsList) 219 { 220 if (icsEvents.hasEvents()) 221 { 222 Tag tag = icsEvents.getTag(); 223 if (filteredTags.isEmpty() || tag != null && filteredTags.contains(tag.getName())) 224 { 225 for (VEvent calendarComponent : icsEvents.getEvents()) 226 { 227 228 TimeZone startDateTZ = calendarComponent.getStartDate().getTimeZone(); 229 if (startDateTZ != null) 230 { 231 if (!timeZoneIds.contains(startDateTZ.getID())) 232 { 233 timeZoneIds.add(startDateTZ.getID()); 234 235 timeZonesAsString += startDateTZ.getVTimeZone().toString() + "\n"; 236 } 237 } 238 239 TimeZone endDateTZ = calendarComponent.getEndDate().getTimeZone(); 240 if (endDateTZ != null) 241 { 242 if (!timeZoneIds.contains(endDateTZ.getID())) 243 { 244 timeZoneIds.add(endDateTZ.getID()); 245 246 timeZonesAsString += endDateTZ.getVTimeZone().toString() + "\n"; 247 } 248 } 249 } 250 } 251 } 252 } 253 254 return timeZonesAsString; 255 } 256 257 /** 258 * SAX ics events hits 259 * @param handler the content handler 260 * @param icsEvents The ics events 261 * @param startNumber the start index 262 * @throws SAXException if an error occurred while saxing 263 */ 264 public void saxIcsEventHits(ContentHandler handler, List<LocalVEvent> icsEvents, int startNumber) throws SAXException 265 { 266 int hitIndex = startNumber; 267 for (LocalVEvent icsEvent : icsEvents) 268 { 269 saxIcsEventHit(handler, icsEvent, hitIndex++); 270 } 271 } 272 273 /** 274 * SAX a ics events hit 275 * @param handler the content handler 276 * @param icsEvent The ics event 277 * @param number the hit index 278 * @throws SAXException if an error occurred while saxing 279 */ 280 public void saxIcsEventHit(ContentHandler handler, LocalVEvent icsEvent, int number) throws SAXException 281 { 282 VEvent event = icsEvent.getEvent(); 283 284 AttributesImpl attrs = new AttributesImpl(); 285 attrs.addCDATAAttribute("number", Integer.toString(number)); 286 attrs.addCDATAAttribute("icsEvent", "true"); 287 XMLUtils.startElement(handler, "hit", attrs); 288 289 String id = event.getProperty(Property.UID) != null ? event.getProperty(Property.UID).getValue() : UUID.randomUUID().toString(); 290 XMLUtils.createElement(handler, "id", id); 291 292 saxIcsEvent(handler, icsEvent); 293 294 XMLUtils.endElement(handler, "hit"); 295 } 296 297 /** 298 * SAX a ics event 299 * @param handler the content handler 300 * @param icsEvent The ics event 301 * @throws SAXException if an error occurred while saxing 302 */ 303 public void saxIcsEvent(ContentHandler handler, LocalVEvent icsEvent) throws SAXException 304 { 305 XMLUtils.startElement(handler, "event"); 306 307 VEvent event = icsEvent.getEvent(); 308 309 String title = event.getProperty(Property.SUMMARY) != null ? event.getProperty(Property.SUMMARY).getValue() : StringUtils.EMPTY; 310 XMLUtils.createElement(handler, "title", title); 311 312 String description = event.getProperty(Property.DESCRIPTION) != null ? event.getProperty(Property.DESCRIPTION).getValue() : StringUtils.EMPTY; 313 XMLUtils.createElement(handler, "description", description); 314 315 Date dtStamp = event.getDateStamp() != null ? event.getDateStamp().getDate() : new Date(); 316 Date creationDate = event.getCreated() != null ? event.getCreated().getDate() : dtStamp; 317 Date lastModifiedDate = event.getLastModified() != null ? event.getLastModified().getDate() : dtStamp; 318 319 _saxDate(handler, "creationDate", creationDate); 320 _saxDate(handler, "lastModifiedDate", lastModifiedDate); 321 322 _saxDate(handler, "startDate", icsEvent.getStart()); 323 _saxDate(handler, "endDate", icsEvent.getEnd()); 324 325 _saxAllDay(handler, icsEvent); 326 327 CalendarSearchService.saxTag(handler, icsEvent.getTag()); 328 329 Url url = event.getUrl(); 330 if (url != null) 331 { 332 XMLUtils.createElement(handler, "url", url.getValue()); 333 } 334 335 XMLUtils.endElement(handler, "event"); 336 337 } 338 339 /** 340 * Sax all day property 341 * @param handler the content handler 342 * @param icsEvent the ICS event 343 * @throws SAXException if an error occurred while saxing 344 */ 345 protected void _saxAllDay(ContentHandler handler, LocalVEvent icsEvent) throws SAXException 346 { 347 VEvent event = icsEvent.getEvent(); 348 349 DtStart startDate = event.getStartDate(); 350 DtEnd endDate = event.getEndDate(); 351 352 boolean allDayEvent = startDate != null && startDate.getParameter("VALUE") != null && "DATE".equals(startDate.getParameter("VALUE").getValue()) 353 && (endDate == null 354 || endDate.getParameter("VALUE") != null && "DATE".equals(endDate.getParameter("VALUE").getValue()) 355 ); 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 date the date to sax 365 * @throws SAXException if an error occurred while saxing 366 */ 367 protected void _saxDate(ContentHandler handler, String tagName, Date date) throws SAXException 368 { 369 if (date != null) 370 { 371 XMLUtils.createElement(handler, tagName, DateUtils.dateToString(date)); 372 } 373 } 374 375 /** 376 * Sax ICS errors 377 * @param icsResults the ICS events 378 * @param handler the content handler 379 * @throws SAXException if an error occurred 380 */ 381 public void saxICSErrors(List<IcsEvents> icsResults, ContentHandler handler) throws SAXException 382 { 383 List<IcsEvents> errorsIcs = icsResults.stream() 384 .filter(ics -> ics.getStatus() != IcsEvents.Status.OK) 385 .collect(Collectors.toList()); 386 387 if (!errorsIcs.isEmpty()) 388 { 389 XMLUtils.startElement(handler, "errors-ics"); 390 for (IcsEvents ics : errorsIcs) 391 { 392 AttributesImpl attrs = new AttributesImpl(); 393 attrs.addCDATAAttribute("url", ics.getUrl()); 394 attrs.addCDATAAttribute("status", ics.getStatus().name()); 395 XMLUtils.createElement(handler, "ics", attrs); 396 } 397 XMLUtils.endElement(handler, "errors-ics"); 398 } 399 } 400}