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