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}