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