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}