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.Url;
051
052/**
053 * Helper from ICS events
054 */
055public class IcsEventHelper implements Component, Serviceable
056{
057    /** The component role. */
058    public static final String ROLE = IcsEventHelper.class.getName();
059    
060    private IcsReader _icsReader;
061    private TagProviderExtensionPoint _tagProviderEP;
062
063    public void service(ServiceManager smanager) throws ServiceException
064    {
065        _icsReader = (IcsReader) smanager.lookup(IcsReader.ROLE);
066        _tagProviderEP = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE);
067    }
068    
069    /**
070     * Read the configured distant ics sources into a list of {@link IcsEvents}
071     * @param zoneItem zoneItem where the configuration will be fetched
072     * @param siteName name of the current site
073     * @param dateRange range of dates to limit
074     * @return a list of {@link IcsEvents}
075     */
076    public List<IcsEvents> getICSEvents(ZoneItem zoneItem, String siteName, EventsFilterHelper.DateTimeRange dateRange)
077    {
078        List<IcsEvents> icsEvents = new ArrayList<>();
079        if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics"))
080        {
081            Long nbIcsEvent = zoneItem.getServiceParameters().getValue("nbEvents");
082            Long maxIcsSize = zoneItem.getServiceParameters().getValue("maxSize");
083    
084            ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics");
085            for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries())
086            {
087                String url = repeaterEntry.getValue("url");
088                IcsEvents eventList = _icsReader.getEventList(url, dateRange, nbIcsEvent, maxIcsSize);
089                
090                String tagName = repeaterEntry.getValue("tag");
091                if (StringUtils.isNotEmpty(tagName))
092                {
093                    Tag tag = _tagProviderEP.getTag(tagName, Map.of("siteName", siteName));
094                    eventList.setTag(tag);
095                }
096
097                icsEvents.add(eventList);
098            }
099        }
100        return icsEvents;
101    }
102    
103    /**
104     * Get the ICS tags from search service
105     * @param zoneItem The zone item id
106     * @param siteName the site name
107     * @return the ICS tags
108     */
109    public Set<Tag> getIcsTags(ZoneItem zoneItem, String siteName)
110    {
111        Set<Tag> tags = new LinkedHashSet<>();
112        
113        if (zoneItem != null && zoneItem.getServiceParameters().hasValue("ics"))
114        {
115            ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics");
116            for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries())
117            {
118                String tagName = repeaterEntry.getValue("tag");
119                Tag tag = _tagProviderEP.getTag(tagName, Map.of("siteName", siteName));
120                if (tag != null)
121                {
122                    tags.add(tag);
123                }
124            }
125        }
126        
127        return tags;
128    }
129    
130    /**
131     * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents}
132     * @param icsEventsList the list of {@link IcsEvents}
133     * @param dateRange range of dates to limit
134     * @return a list of {@link LocalVEvent}
135     */
136    public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange)
137    {
138        return toLocalIcsEvent(icsEventsList, dateRange, List.of());
139    }
140    
141    /**
142     * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents}
143     * @param icsEventsList the list of {@link IcsEvents}
144     * @param dateRange range of dates to limit
145     * @param filteredTags A list of tag's name to filter ICS events. Can be empty to no filter on tags.
146     * @return a list of {@link LocalVEvent}
147     */
148    public Pair<List<LocalVEvent>, String> toLocalIcsEvent(List<IcsEvents> icsEventsList, EventsFilterHelper.DateTimeRange dateRange, List<String> filteredTags)
149    {
150        List<LocalVEvent> localICSEvents = new ArrayList<>();
151        String fullICSDistantEvents = "";
152        for (IcsEvents icsEvents : icsEventsList)
153        {
154            if (icsEvents.hasEvents())
155            {
156                Tag tag = icsEvents.getTag();
157                if (filteredTags.isEmpty() || tag != null && filteredTags.contains(tag.getName()))
158                {
159                    for (VEvent calendarComponent : icsEvents.getEvents())
160                    {
161                        List<LocalVEvent> localCalendarComponents = _icsReader.getEventDates(calendarComponent, dateRange, tag);
162                        localICSEvents.addAll(localCalendarComponents);
163                        if (dateRange == null)
164                        {
165                            // 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
166                            fullICSDistantEvents += calendarComponent.toString() + "\n";
167                        }
168                    }
169                }
170            }
171        }
172        
173        return new ImmutablePair<>(localICSEvents, fullICSDistantEvents);
174    }
175    
176    /**
177     * SAX ics events hits
178     * @param handler the content handler
179     * @param icsEvents The ics events
180     * @param startNumber the start index
181     * @throws SAXException if an error occurred while saxing
182     */
183    public void saxIcsEventHits(ContentHandler handler, List<LocalVEvent> icsEvents, int startNumber) throws SAXException
184    {
185        int hitIndex = startNumber;
186        for (LocalVEvent icsEvent : icsEvents)
187        {
188            saxIcsEventHit(handler, icsEvent, hitIndex++);
189        }
190    }
191    
192    /**
193     * SAX a ics events hit
194     * @param handler the content handler
195     * @param icsEvent The ics event
196     * @param number the hit index
197     * @throws SAXException if an error occurred while saxing
198     */
199    public void saxIcsEventHit(ContentHandler handler, LocalVEvent icsEvent, int number) throws SAXException
200    {
201        VEvent event = icsEvent.getEvent();
202        
203        AttributesImpl attrs = new AttributesImpl();
204        attrs.addCDATAAttribute("number", Integer.toString(number));
205        attrs.addCDATAAttribute("icsEvent", "true");
206        XMLUtils.startElement(handler, "hit", attrs);
207
208        String id = event.getProperty(Property.UID) != null ? event.getProperty(Property.UID).getValue() : UUID.randomUUID().toString();
209        XMLUtils.createElement(handler, "id", id);
210        
211        saxIcsEvent(handler, icsEvent);
212        
213        XMLUtils.endElement(handler, "hit");
214    }
215    
216    /**
217     * SAX a ics event
218     * @param handler the content handler
219     * @param icsEvent The ics event
220     * @throws SAXException if an error occurred while saxing
221     */
222    public void saxIcsEvent(ContentHandler handler, LocalVEvent icsEvent) throws SAXException
223    {
224        XMLUtils.startElement(handler, "event");
225        
226        VEvent event = icsEvent.getEvent();
227        
228        String title = event.getProperty(Property.SUMMARY) != null ? event.getProperty(Property.SUMMARY).getValue() : StringUtils.EMPTY;
229        XMLUtils.createElement(handler, "title", title);
230        
231        String description = event.getProperty(Property.DESCRIPTION) != null ? event.getProperty(Property.DESCRIPTION).getValue() : StringUtils.EMPTY;
232        XMLUtils.createElement(handler, "description", description);
233        
234        Date dtStamp = event.getDateStamp() != null ? event.getDateStamp().getDate() : new Date();
235        Date creationDate = event.getCreated() != null ? event.getCreated().getDate() : dtStamp;
236        Date lastModifiedDate = event.getLastModified() != null ? event.getLastModified().getDate() : dtStamp;
237        
238        _saxDate(handler, "creationDate", creationDate);
239        _saxDate(handler, "lastModifiedDate", lastModifiedDate);
240        
241        _saxDate(handler, "startDate", icsEvent.getStart());
242        _saxDate(handler, "endDate", icsEvent.getEnd());
243        
244        CalendarSearchService.saxTag(handler, icsEvent.getTag());
245        
246        Url url = event.getUrl();
247        if (url != null)
248        {
249            XMLUtils.createElement(handler, "url", url.getValue());
250        }
251        
252        XMLUtils.endElement(handler, "event");
253        
254    }
255    
256    private void _saxDate(ContentHandler handler, String tagName, Date date) throws SAXException
257    {
258        if (date != null)
259        {
260            XMLUtils.createElement(handler, tagName, DateUtils.dateToString(date));
261        }
262    }
263}