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}