001/*
002 *  Copyright 2012 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.events;
017
018import java.io.IOException;
019import java.time.LocalDate;
020import java.time.ZoneId;
021import java.time.ZonedDateTime;
022import java.time.format.DateTimeFormatter;
023import java.time.temporal.ChronoField;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.Set;
036import java.util.UUID;
037import java.util.stream.Collectors;
038
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.cocoon.ProcessingException;
042import org.apache.cocoon.environment.ObjectModelHelper;
043import org.apache.cocoon.environment.Request;
044import org.apache.cocoon.xml.AttributesImpl;
045import org.apache.cocoon.xml.XMLUtils;
046import org.apache.commons.lang.StringUtils;
047import org.apache.commons.lang.time.DateUtils;
048import org.apache.commons.lang3.tuple.ImmutablePair;
049import org.apache.commons.lang3.tuple.Pair;
050import org.xml.sax.ContentHandler;
051import org.xml.sax.SAXException;
052
053import org.ametys.cms.repository.Content;
054import org.ametys.cms.tag.Tag;
055import org.ametys.cms.tag.TagProviderExtensionPoint;
056import org.ametys.core.util.URIUtils;
057import org.ametys.plugins.calendar.icsreader.IcsReader;
058import org.ametys.plugins.calendar.icsreader.IcsReader.IcsEvents;
059import org.ametys.plugins.calendar.icsreader.LocalVEvent;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
063import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
064import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.web.WebConstants;
067import org.ametys.web.content.GetSiteAction;
068import org.ametys.web.filter.WebContentFilter;
069import org.ametys.web.filter.WebContentFilter.AccessLimitation;
070import org.ametys.web.repository.page.Page;
071import org.ametys.web.repository.page.ZoneItem;
072
073import net.fortuna.ical4j.model.Property;
074import net.fortuna.ical4j.model.component.VEvent;
075
076/**
077 * Query and generate news according to many parameters.
078 */
079public class EventsGenerator extends AbstractEventGenerator
080{
081    /** The ametys object resolver. */
082    protected AmetysObjectResolver _ametysResolver;
083
084    /** The events helper */
085    protected EventsFilterHelper _eventsFilterHelper;
086    
087    /** The ICS Reader */
088    protected IcsReader _icsReader;
089
090    /** The tag provider extension point. */
091    protected TagProviderExtensionPoint _tagProviderEP;
092
093    @Override
094    public void service(ServiceManager serviceManager) throws ServiceException
095    {
096        super.service(serviceManager);
097        _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
098        _eventsFilterHelper = (EventsFilterHelper) serviceManager.lookup(EventsFilterHelper.ROLE);
099        _icsReader = (IcsReader) serviceManager.lookup(IcsReader.ROLE);
100        _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE);
101    }
102
103    @Override
104    public void generate() throws IOException, SAXException, ProcessingException
105    {
106        Request request = ObjectModelHelper.getRequest(objectModel);
107        @SuppressWarnings("unchecked")
108        Map<String, Object> parentContextAttrs = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
109        if (parentContextAttrs == null)
110        {
111            parentContextAttrs = Collections.EMPTY_MAP;
112        }
113
114        LocalDate today = LocalDate.now();
115
116        // Get site and language in sitemap parameters. Can not be null.
117        String siteName = parameters.getParameter("site", (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME));
118        String lang = parameters.getParameter("lang", (String) request.getAttribute("renderingLanguage"));
119        if (StringUtils.isEmpty(lang))
120        {
121            lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
122        }
123        // Get the parameters.
124        int monthsBefore = parameters.getParameterAsInteger("months-before", 3);
125        int monthsAfter = parameters.getParameterAsInteger("months-after", 3);
126        // Type can be "calendar", "single-day" or "agenda".
127        String type = parameters.getParameter("type", "calendar");
128        String view = parameters.getParameter("view", "");
129        int year = parameters.getParameterAsInteger("year", today.getYear());
130        int month = parameters.getParameterAsInteger("month", today.getMonthValue());
131        int day = parameters.getParameterAsInteger("day", today.getDayOfMonth());
132        // Select a single tag or "all".
133        String requestedTagsString = parameters.getParameter("tags", "all");
134        
135        
136        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
137        // Get the zone item, as a request attribute or from the ID in the
138        // parameters.
139        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
140        String zoneItemId = parameters.getParameter("zoneItemId", "");
141        if (zoneItem == null && StringUtils.isNotEmpty(zoneItemId))
142        {
143            zoneItemId = URIUtils.decode(zoneItemId);
144            zoneItem = (ZoneItem) _ametysResolver.resolveById(zoneItemId);
145        }
146        
147        if (page == null && zoneItem != null)
148        {
149            // Wrapped page such as _plugins/calendar/page/YEAR/MONTH/DAY/ZONEITEMID/events_1.3.html => get the page from its zone item
150            // The page is needed to get restriction
151            page = zoneItem.getZone().getPage();
152        }
153        
154        ZonedDateTime dateTime = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, ZoneId.systemDefault());
155        String title = _eventsFilterHelper.getTitle(zoneItem);
156        String rangeType = parameters.getParameter("rangeType", _eventsFilterHelper.getDefaultRangeType(zoneItem));
157        boolean maskOrphan = _eventsFilterHelper.getMaskOrphan(zoneItem);
158        boolean pdfDownload = _eventsFilterHelper.getPdfDownload(zoneItem);
159        boolean icalDownload = _eventsFilterHelper.getIcalDownload(zoneItem);
160        String link = _eventsFilterHelper.getLink(zoneItem);
161        String linkTitle = _eventsFilterHelper.getLinkTitle(zoneItem);
162        
163        boolean doRetrieveView = !StringUtils.equalsIgnoreCase("false", parameters.getParameter("do-retrieve-view", "true"));
164
165        // Get the search context to match, from the zone item or from the parameters.
166        @SuppressWarnings("unchecked")
167        List<Map<String, Object>> searchContexts = _eventsFilterHelper.getSearchContext(zoneItem, (List<Map<String, Object>>) parentContextAttrs.get("search"));
168        
169        
170        Set<String> tags = _eventsFilterHelper.getTags(zoneItem, searchContexts);
171        Set<Tag> categories = _eventsFilterHelper.getTagCategories(zoneItem, searchContexts, siteName);
172        Set<Tag> icsTags = _getIcsTags(zoneItem, siteName);
173        String pagePath = page != null ? page.getPathInSitemap() : "";
174        
175        Set<String> filteredCategories = _eventsFilterHelper.getFilteredCategories(null, requestedTagsString.split(","), zoneItem, siteName);
176        // Get the date range and deduce the expression (single day or month-before to month-after).
177        EventsFilterHelper.DateRange dateRange = _eventsFilterHelper.getDateRange(type, year, month, day, monthsBefore, monthsAfter, rangeType);
178        
179        EventsFilter eventsFilter = _eventsFilterHelper.generateEventFilter(dateRange, zoneItem, view, type, filteredCategories, searchContexts);
180        
181        // Get the corresponding contents.
182        AmetysObjectIterable<Content> eventContents = eventsFilter.getMatchingContents(siteName, lang, page);
183        
184        // Read ICS threads
185        List<IcsEvents> parsedICS = _getICSEvents(zoneItem, siteName, dateRange);
186        
187        // CAL-94 (same as CMS-2292 for filtered contents)
188        String currentSiteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME);
189        String currentSkinName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SKIN_ID);
190        String currentTemplateName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_TEMPLATE_ID);
191        String currentLanguage = (String) request.getAttribute("renderingLanguage");
192        request.setAttribute(GetSiteAction.OVERRIDE_SITE_REQUEST_ATTR, currentSiteName);
193        request.setAttribute(GetSiteAction.OVERRIDE_SKIN_REQUEST_ATTR, currentSkinName);
194
195        try
196        {
197            _sax(today, monthsBefore, monthsAfter, year, month, day, filteredCategories, page, zoneItem, dateTime, title, rangeType, maskOrphan, pdfDownload, icalDownload, link, linkTitle, doRetrieveView, tags, categories, icsTags, pagePath, eventsFilter, dateRange, eventContents, parsedICS);
198        }
199        finally
200        {
201            request.removeAttribute(GetSiteAction.OVERRIDE_SITE_REQUEST_ATTR);
202            request.removeAttribute(GetSiteAction.OVERRIDE_SKIN_REQUEST_ATTR);
203            request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, currentSiteName);
204            request.setAttribute("siteName", currentSiteName);
205            request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, currentSkinName);
206            request.setAttribute(WebConstants.REQUEST_ATTR_TEMPLATE_ID, currentTemplateName);
207            request.setAttribute("renderingLanguage", currentLanguage);
208        }
209    }
210    
211    private Set<Tag> _getIcsTags(ZoneItem zoneItem, String currentSiteName)
212    {
213        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
214        Set<Tag> categories = new LinkedHashSet<>();
215        
216        // Add the categories defined in the ICS fields
217        if (serviceParameters.hasNonEmptyValue("ics"))
218        {
219            ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics");
220            for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries())
221            {
222                String categoryName = repeaterEntry.getValue("tag");
223                Map<String, Object> contextualParameters = new HashMap<>();
224                contextualParameters.put("siteName", currentSiteName);
225                Tag category = _tagProviderEP.getTag(categoryName, contextualParameters);
226                if (category != null)
227                {
228                    categories.add(category);
229                }
230            }
231        }
232        return categories;
233    }
234
235    /**
236     * Read the configured distant ics sources into a list of {@link IcsEvents}
237     * @param zoneItem zoneItem where the configuration will be fetched
238     * @param siteName name of the current site
239     * @param dateRange range of dates to limit
240     * @return a list of {@link IcsEvents}
241     */
242    private List<IcsEvents> _getICSEvents(ZoneItem zoneItem, String siteName, EventsFilterHelper.DateRange dateRange)
243    {
244        List<IcsEvents> icsEvents = new ArrayList<>();
245        if (zoneItem != null && zoneItem.getServiceParameters().hasNonEmptyValue("ics"))
246        {
247            Long nbIcsEvent = zoneItem.getServiceParameters().getValue("nbEvents");
248            Long maxIcsSize = zoneItem.getServiceParameters().getValue("maxSize");
249    
250            ModifiableModelAwareRepeater icsRepeater = zoneItem.getServiceParameters().getValue("ics");
251            for (ModifiableModelAwareRepeaterEntry repeaterEntry : icsRepeater.getEntries())
252            {
253                String url = repeaterEntry.getValue("url");
254                IcsEvents eventList = _icsReader.getEventList(url, dateRange, nbIcsEvent, maxIcsSize);
255                
256                String tagName = repeaterEntry.getValue("tag");
257                Map<String, Object> tagParameters = new HashMap<>();
258                tagParameters.put("siteName", siteName);
259                Tag tag = _tagProviderEP.getTag(tagName, tagParameters);
260                eventList.setTag(tag);
261
262                icsEvents.add(eventList);
263            }
264        }
265        return icsEvents;
266    }
267    
268    /**
269     * Get a list of {@link LocalVEvent} form the list of {@link IcsEvents}
270     * @param icsEventsList the list of {@link IcsEvents}
271     * @param dateRange range of dates to limit
272     * @return a list of {@link LocalVEvent}
273     */
274    private Pair<List<LocalVEvent>, String> _getLocalICSEvents(List<IcsEvents> icsEventsList, EventsFilterHelper.DateRange dateRange)
275    {
276        List<LocalVEvent> localICSEvents = new ArrayList<>();
277        String fullICSDistantEvents = "";
278        for (IcsEvents icsEvents : icsEventsList)
279        {
280            if (icsEvents.hasEvents())
281            {
282                Tag tag = icsEvents.getTag();
283                for (VEvent calendarComponent : icsEvents.getEvents())
284                {
285                    List<LocalVEvent> localCalendarComponents = _icsReader.getEventDates(calendarComponent, dateRange, tag);
286                    localICSEvents.addAll(localCalendarComponents);
287                    if (dateRange == null)
288                    {
289                        // 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
290                        fullICSDistantEvents += calendarComponent.toString() + "\n";
291                    }
292                }
293            }
294        }
295        
296        return new ImmutablePair<>(localICSEvents, fullICSDistantEvents);
297    }
298
299    private void _sax(LocalDate today, int monthsBefore, int monthsAfter, int year, int month, int day, Set<String> filteredCategoryTags, Page page, ZoneItem zoneItem, ZonedDateTime dateTime,
300            String title, String rangeType, boolean maskOrphan, boolean pdfDownload, boolean icalDownload, String link, String linkTitle, boolean doRetrieveView, Set<String> tags,
301            Set<Tag> categories, Set<Tag> icsTags, String pagePath, EventsFilter eventsFilter, EventsFilterHelper.DateRange dateRange, AmetysObjectIterable<Content> eventContents, List<IcsEvents> icsEvents)
302            throws SAXException, IOException
303    {
304        AttributesImpl atts = new AttributesImpl();
305
306        atts.addCDATAAttribute("page-path", pagePath);
307        atts.addCDATAAttribute("today", DateTimeFormatter.ISO_LOCAL_DATE.format(today));
308        if (dateRange != null)
309        {
310            if (dateRange.getStartDate() != null)
311            {
312                atts.addCDATAAttribute("start", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault())));
313            }
314            if (dateRange.getEndDate() != null)
315            {
316                atts.addCDATAAttribute("end", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault())));
317            }
318        }
319
320        atts.addCDATAAttribute("year", Integer.toString(year));
321        atts.addCDATAAttribute("month", String.format("%02d", month));
322        atts.addCDATAAttribute("day", String.format("%02d", day));
323        atts.addCDATAAttribute("months-before", Integer.toString(monthsBefore));
324        atts.addCDATAAttribute("months-after", Integer.toString(monthsAfter));
325
326        atts.addCDATAAttribute("title", title);
327        atts.addCDATAAttribute("mask-orphan", Boolean.toString(maskOrphan));
328        atts.addCDATAAttribute("pdf-download", Boolean.toString(pdfDownload));
329        atts.addCDATAAttribute("ical-download", Boolean.toString(icalDownload));
330        atts.addCDATAAttribute("link", link);
331        atts.addCDATAAttribute("link-title", linkTitle);
332
333        if (zoneItem != null)
334        {
335            atts.addCDATAAttribute("zoneItemId", zoneItem.getId());
336        }
337        if (StringUtils.isNotEmpty(rangeType))
338        {
339            atts.addCDATAAttribute("range", rangeType);
340        }
341        
342        if (!filteredCategoryTags.isEmpty())
343        {
344            atts.addCDATAAttribute("requested-tags", String.join(",", filteredCategoryTags));
345        }
346
347        contentHandler.startDocument();
348        XMLUtils.startElement(contentHandler, "events", atts);
349
350        _saxRssUrl(zoneItem);
351
352        // Generate months (used in calendar mode) and days (used in full-page
353        // agenda mode).
354        _saxMonths(dateRange);
355        _saxDays(dateTime, rangeType);
356
357        _saxDaysNew(dateRange, rangeType);
358
359        // Generate tags and categories.
360        _saxTags(tags);
361        _saxCategories(categories, icsTags);
362
363        Pair<List<LocalVEvent>, String> parsedICSEvents = _getLocalICSEvents(icsEvents, dateRange);
364        List<LocalVEvent> localIcsEvents = parsedICSEvents.getLeft();
365        String fullICSDistantEvents = parsedICSEvents.getRight();
366        
367        // Generate the matching contents.
368        XMLUtils.startElement(contentHandler, "contents");
369
370        saxMatchingContents(contentHandler, eventsFilter, eventContents, page, doRetrieveView);
371
372        saxIcsEvents(contentHandler, localIcsEvents);
373
374        XMLUtils.endElement(contentHandler, "contents");
375
376        XMLUtils.createElement(contentHandler, "rawICS", fullICSDistantEvents);
377        
378        // Generate ICS events with errors
379        _saxErrorsICS(icsEvents);
380        
381        XMLUtils.endElement(contentHandler, "events");
382        contentHandler.endDocument();
383    }
384
385    private void _saxErrorsICS(List<IcsEvents> icsEvents) throws SAXException
386    {
387        List<IcsEvents> errorsIcs = icsEvents.stream()
388            .filter(ics -> ics.getStatus() != IcsEvents.Status.OK)
389            .collect(Collectors.toList());
390
391        if (!errorsIcs.isEmpty())
392        {
393            XMLUtils.startElement(contentHandler, "errors-ics");
394            for (IcsEvents ics : errorsIcs)
395            {
396                AttributesImpl attrs = new AttributesImpl();
397                attrs.addCDATAAttribute("url", ics.getUrl());
398                attrs.addCDATAAttribute("status", ics.getStatus().name());
399                XMLUtils.createElement(contentHandler, "ics", attrs);
400            }
401            XMLUtils.endElement(contentHandler, "errors-ics");
402        }
403    }
404
405    private void _saxRssUrl(ZoneItem zoneItem) throws SAXException 
406    {
407        if (zoneItem != null)
408        {
409            ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
410            // First check that there is a value because calendar service doesn't define the rss parameter
411            if (serviceParameters.hasValue("rss") && (boolean) serviceParameters.getValue("rss"))
412            {
413                // Only add RSS if there is a search context
414                if (serviceParameters.hasValue("search"))
415                {
416                    ModifiableModelAwareRepeater searchRepeater = serviceParameters.getValue("search");
417                    if (searchRepeater.getSize() > 0)
418                    {
419                        // Split protocol and id
420                        String[] zoneItemId = zoneItem.getId().split("://");
421                        String url = "_plugins/calendar/" + zoneItemId[1] + "/rss.xml";
422                        
423                        XMLUtils.createElement(contentHandler, "rssUrl", url);
424                    }
425                }
426            }
427        }
428    }
429    
430    /**
431     * SAX all contents matching the given filter
432     * 
433     * @param handler The content handler to SAX into
434     * @param filter The filter
435     * @param contents iterator on the contents.
436     * @param currentPage The current page.
437     * @param saxContentItSelf true to sax the content, false will only sax some meta
438     * @throws SAXException If an error occurs while SAXing
439     * @throws IOException If an error occurs while retrieving content.
440     */
441    public void saxMatchingContents(ContentHandler handler, WebContentFilter filter, AmetysObjectIterable<Content> contents, Page currentPage, boolean saxContentItSelf) throws SAXException, IOException
442    {
443        boolean checkUserAccess = filter.getAccessLimitation() == AccessLimitation.USER_ACCESS;
444        
445        for (Content content : contents)
446        {
447            if (_filterHelper.isContentValid(content, currentPage, filter))
448            {
449                saxContent(handler, content, saxContentItSelf, filter, checkUserAccess);
450            }
451        }
452    }
453    
454    /**
455     * Sax a list of events coming from a distant ICS file
456     * @param handler The content handler to SAX into
457     * @param icsEvents the events to sax
458     * @throws SAXException Something went wrong
459     */
460    public void saxIcsEvents(ContentHandler handler, List<LocalVEvent> icsEvents) throws SAXException
461    {
462        for (LocalVEvent icsEvent : icsEvents)
463        {
464            saxIcsEvent(handler, icsEvent);
465        }
466    }
467    
468    /**
469     * Sax an event coming from a distant ICS file
470     * @param handler The content handler to SAX into
471     * @param icsEvent an event to sax
472     * @throws SAXException Something went wrong
473     */
474    public void saxIcsEvent(ContentHandler handler, LocalVEvent icsEvent) throws SAXException
475    {
476        VEvent event = icsEvent.getEvent();
477        AttributesImpl attrs = new AttributesImpl();
478
479        String start = org.ametys.core.util.DateUtils.dateToString(icsEvent.getStart());
480        String end = org.ametys.core.util.DateUtils.dateToString(icsEvent.getEnd());
481        List<String> params = new ArrayList<>();
482        
483        String title = event.getProperty(Property.SUMMARY) != null ? event.getProperty(Property.SUMMARY).getValue() : "";
484        String id = event.getProperty(Property.UID) != null ? event.getProperty(Property.UID).getValue() : UUID.randomUUID().toString();
485        String eventAbstract = event.getProperty(Property.DESCRIPTION) != null ? event.getProperty(Property.DESCRIPTION).getValue() : "";
486        
487        params.add(title);
488
489        if (start != null)
490        {
491            String startAttr = org.ametys.core.util.DateUtils.asZonedDateTime(icsEvent.getStart(), null).format(DateTimeFormatter.ISO_LOCAL_DATE);
492            params.add(start);
493            attrs.addCDATAAttribute("start", startAttr);
494        }
495        
496        if (end != null)
497        {
498            String endAttr = org.ametys.core.util.DateUtils.asZonedDateTime(icsEvent.getEnd(), null).format(DateTimeFormatter.ISO_LOCAL_DATE);
499            params.add(end);
500            attrs.addCDATAAttribute("end", endAttr);
501        }
502
503        XMLUtils.startElement(handler, "event", attrs);
504
505        String key = end == null ? "CALENDAR_SERVICE_AGENDA_EVENT_TITLE_SINGLE_DAY" : "CALENDAR_SERVICE_AGENDA_FROM_TO";
506        I18nizableText description = new I18nizableText(null, key, params);
507        description.toSAX(handler, "description");
508        
509        attrs = new AttributesImpl();
510        attrs.addCDATAAttribute("id", "ics://" + id);
511        attrs.addCDATAAttribute("title", title);
512        
513        Date dtStamp = event.getDateStamp() != null ? event.getDateStamp().getDate() : new Date();
514        Date createdAtDate = event.getCreated() != null ? event.getCreated().getDate() : dtStamp;
515        Date lastModifiedDate = event.getLastModified() != null ? event.getLastModified().getDate() : dtStamp;
516
517        String createdAt =  org.ametys.core.util.DateUtils.asZonedDateTime(createdAtDate, null).format(DateTimeFormatter.ISO_INSTANT);
518        attrs.addCDATAAttribute("createdAt", createdAt);
519
520        String lastModified =  org.ametys.core.util.DateUtils.asZonedDateTime(lastModifiedDate, null).format(DateTimeFormatter.ISO_INSTANT);
521        attrs.addCDATAAttribute("lastModifiedAt", lastModified);
522        
523        XMLUtils.startElement(handler, "content", attrs);
524
525        XMLUtils.startElement(handler, "metadata");
526        attrs = new AttributesImpl();
527        attrs.addCDATAAttribute("typeId", "string");
528        attrs.addCDATAAttribute("multiple", "false");
529        XMLUtils.createElement(handler, "title", attrs, title);
530        XMLUtils.createElement(handler, "abstract", attrs, eventAbstract);
531        
532
533        attrs = new AttributesImpl();
534        attrs.addCDATAAttribute("typeId", "datetime");
535        attrs.addCDATAAttribute("multiple", "false");
536        XMLUtils.createElement(handler, "start-date", attrs, start);
537        XMLUtils.createElement(handler, "end-date", attrs, end);
538
539        XMLUtils.endElement(handler, "metadata");
540
541        Tag tag = icsEvent.getTag();
542        if (tag != null)
543        {
544            XMLUtils.startElement(handler, "tags");
545            attrs = new AttributesImpl();
546            attrs.addCDATAAttribute("parent", tag.getParentName());
547            XMLUtils.startElement(handler, tag.getName(), attrs);
548            tag.getTitle().toSAX(handler);
549            XMLUtils.endElement(handler, tag.getName());
550            XMLUtils.endElement(handler, "tags");
551        }
552        
553        XMLUtils.endElement(handler, "content");
554        XMLUtils.endElement(handler, "event");
555    }
556
557    /**
558     * SAX information on the months spanning the date range.
559     * @param dateRange the date range.
560     * @throws SAXException if a error occurs while saxing
561     */
562    protected void _saxMonths(EventsFilterHelper.DateRange dateRange) throws SAXException
563    {
564        if (dateRange != null && dateRange.getStartDate() != null && dateRange.getEndDate() != null)
565        {
566            AttributesImpl atts = new AttributesImpl();
567
568            XMLUtils.startElement(contentHandler, "months");
569
570            ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault());
571            ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault());
572
573            while (date.isBefore(end))
574            {
575                int year = date.getYear();
576                int month = date.getMonthValue();
577
578                String monthStr = String.format("%d-%02d", year, month);
579                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
580
581                atts.clear();
582                atts.addCDATAAttribute("str", monthStr);
583                atts.addCDATAAttribute("raw", dateStr);
584                XMLUtils.startElement(contentHandler, "month", atts);
585
586                XMLUtils.endElement(contentHandler, "month");
587
588                date = date.plusMonths(1);
589            }
590
591            XMLUtils.endElement(contentHandler, "months");
592        }
593    }
594
595    /**
596     * Generate days to build a "calendar" view.
597     * 
598     * @param dateRange a date belonging to the time span to generate.
599     * @param rangeType the range type, "month" or "week".
600     * @throws SAXException if an error occurs while saxing
601     */
602    protected void _saxDaysNew(EventsFilterHelper.DateRange dateRange, String rangeType) throws SAXException
603    {
604        if (dateRange != null)
605        {
606            XMLUtils.startElement(contentHandler, "calendar-months");
607    
608            ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault());
609            ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault());
610            
611    
612            while (date.isBefore(end))
613            {
614                int year = date.getYear();
615                int month = date.getMonthValue();
616    
617                String monthStr = String.format("%d-%02d", year, month);
618                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
619    
620                AttributesImpl attrs = new AttributesImpl();
621                attrs.addCDATAAttribute("str", monthStr);
622                attrs.addCDATAAttribute("raw", dateStr);
623                attrs.addCDATAAttribute("year", Integer.toString(year));
624                attrs.addCDATAAttribute("month", Integer.toString(month));
625                XMLUtils.startElement(contentHandler, "month", attrs);
626    
627                _saxDays(date, "month");
628    
629                XMLUtils.endElement(contentHandler, "month");
630    
631                date = date.plusMonths(1);
632            }
633    
634            XMLUtils.endElement(contentHandler, "calendar-months");
635        }
636    }
637
638    /**
639     * Generate days to build a "calendar" view.
640     * 
641     * @param date a date belonging to the time span to generate.
642     * @param type the range type, "month" or "week".
643     * @throws SAXException if an error occurs while saxing
644     */
645    protected void _saxDays(ZonedDateTime date, String type) throws SAXException
646    {
647        AttributesImpl attrs = new AttributesImpl();
648
649        int rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
650        ZonedDateTime previousDay = null;
651        ZonedDateTime nextDay = null;
652
653        // Week.
654        if ("week".equals(type))
655        {
656            rangeStyle = DateUtils.RANGE_WEEK_MONDAY;
657
658            // Get the first day of the week.
659            previousDay = date.with(ChronoField.DAY_OF_WEEK, 1);
660            // First day of next week.
661            nextDay = previousDay.plusWeeks(1);
662            // First day of previous week.
663            previousDay = previousDay.minusWeeks(1);
664        }
665        else
666        {
667            rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
668
669            // Get the first day of the month.
670            previousDay = date.with(ChronoField.DAY_OF_MONTH, 1);
671            // First day of previous month.
672            nextDay = previousDay.plusMonths(1);
673            // First day of next month.
674            previousDay = previousDay.minusMonths(1);
675        }
676
677        addNavAttributes(attrs, date, previousDay, nextDay);
678
679        // Get an iterator on the days to be present on the calendar.
680        
681        Iterator<Calendar> days = DateUtils.iterator(Date.from(date.toInstant()), rangeStyle);
682
683        XMLUtils.startElement(contentHandler, "calendar", attrs);
684
685        ZonedDateTime previousWeekDay = date.minusWeeks(1);
686        ZonedDateTime nextWeekDay = date.plusWeeks(1);
687
688        AttributesImpl weekAttrs = new AttributesImpl();
689        addNavAttributes(weekAttrs, date, previousWeekDay, nextWeekDay);
690
691        XMLUtils.startElement(contentHandler, "week", weekAttrs);
692
693        while (days.hasNext())
694        {
695            Calendar dayCal = days.next();
696            ZonedDateTime day = dayCal.toInstant().atZone(dayCal.getTimeZone().toZoneId());
697            String rawDateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(day);
698            String dateStr = DateTimeFormatter.ISO_LOCAL_DATE.format(day);
699            String yearStr = Integer.toString(dayCal.get(Calendar.YEAR));
700            String monthStr = Integer.toString(dayCal.get(Calendar.MONTH) + 1);
701            String dayStr = Integer.toString(dayCal.get(Calendar.DAY_OF_MONTH));
702
703            AttributesImpl dayAttrs = new AttributesImpl();
704
705            dayAttrs.addCDATAAttribute("raw", rawDateStr);
706            dayAttrs.addCDATAAttribute("date", dateStr);
707            dayAttrs.addCDATAAttribute("year", yearStr);
708            dayAttrs.addCDATAAttribute("month", monthStr);
709            dayAttrs.addCDATAAttribute("day", dayStr);
710
711            XMLUtils.createElement(contentHandler, "day", dayAttrs);
712
713            // Break on week on the last day of the week (but not on the last
714            // week).
715            if (dayCal.get(Calendar.DAY_OF_WEEK) == _eventsFilterHelper.getLastDayOfWeek(dayCal) && days.hasNext())
716            {
717                previousWeekDay = day.minusDays(6);
718                nextWeekDay = day.plusDays(8);
719                weekAttrs.clear();
720
721                addNavAttributes(weekAttrs, day, previousWeekDay, nextWeekDay);
722
723                XMLUtils.endElement(contentHandler, "week");
724                XMLUtils.startElement(contentHandler, "week", weekAttrs);
725            }
726        }
727
728        XMLUtils.endElement(contentHandler, "week");
729        XMLUtils.endElement(contentHandler, "calendar");
730    }
731
732    /**
733     * Add nav attributes.
734     * 
735     * @param attrs the attributes object to fill in.
736     * @param current the current date.
737     * @param previousDay the previous date.
738     * @param nextDay the next date.
739     */
740    protected void addNavAttributes(AttributesImpl attrs, ZonedDateTime current, ZonedDateTime previousDay, ZonedDateTime nextDay)
741    {
742        attrs.addCDATAAttribute("current", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(current));
743
744        attrs.addCDATAAttribute("previous", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(previousDay));
745        attrs.addCDATAAttribute("previousYear", Integer.toString(previousDay.getYear()));
746        attrs.addCDATAAttribute("previousMonth", Integer.toString(previousDay.getMonthValue()));
747        attrs.addCDATAAttribute("previousDay", Integer.toString(previousDay.getDayOfMonth()));
748
749        attrs.addCDATAAttribute("next", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(nextDay));
750        attrs.addCDATAAttribute("nextYear", Integer.toString(nextDay.getYear()));
751        attrs.addCDATAAttribute("nextMonth", Integer.toString(nextDay.getMonthValue()));
752        attrs.addCDATAAttribute("nextDay", Integer.toString(nextDay.getDayOfMonth()));
753    }
754
755    /**
756     * Generate the list of selected tags.
757     * @param tags the list of tags.
758     * @throws SAXException if an error occurs while saxing
759     */
760    protected void _saxTags(Collection<String> tags) throws SAXException
761    {
762        XMLUtils.startElement(contentHandler, "tags");
763        for (String tag : tags)
764        {
765            AttributesImpl attrs = new AttributesImpl();
766            attrs.addCDATAAttribute("name", tag);
767            XMLUtils.createElement(contentHandler, "tag", attrs);
768        }
769        XMLUtils.endElement(contentHandler, "tags");
770    }
771
772    /**
773     * Generate the list of selected tags that act as categories and their descendant tags.
774     * @param categories the list of categories to generate.
775     * @param icsTags list of tags for the ICS feeds (tags, not parents)
776     * @throws SAXException if an error occurs while saxing
777     */
778    protected void _saxCategories(Collection<Tag> categories, Collection<Tag> icsTags) throws SAXException
779    {
780        Map<Tag, Set<Tag>> icsTagsToAdd = new HashMap<>();
781        
782        // Only the tags that are not already in the ones from the search contexts
783        for (Tag tag : icsTags)
784        {
785            Tag parent = tag.getParent();
786            if (icsTagsToAdd.containsKey(parent))
787            {
788                icsTagsToAdd.get(parent).add(tag);
789            }
790            else if (categories == null || !categories.contains(tag.getParent()))
791            {
792                icsTagsToAdd.put(parent, new LinkedHashSet<>());
793                icsTagsToAdd.get(parent).add(tag);
794            }
795        }
796        
797        XMLUtils.startElement(contentHandler, "tag-categories");
798        
799     // Add the tags from the search contexts
800        if (categories != null)
801        {
802            for (Tag category : categories)
803            {
804                XMLUtils.startElement(contentHandler, "category");
805    
806                category.getTitle().toSAX(contentHandler, "title");
807    
808                _saxTags(_eventsFilterHelper.getAllTags(category));
809    
810                XMLUtils.endElement(contentHandler, "category");
811            }
812        }
813        
814        // Add the tags from the ICS feeds
815        for (Entry<Tag, Set<Tag>> entry : icsTagsToAdd.entrySet())
816        {
817            Tag parent = entry.getKey();
818            Set<Tag> tags = entry.getValue();
819            
820            XMLUtils.startElement(contentHandler, "category");
821
822            // As in the ICS, we select directly a tag and not a category, it is possible that there are no parent.
823            // To keep the XML equivalent, the category is still created, possibly with an empty title
824            if (parent != null)
825            {
826                parent.getTitle().toSAX(contentHandler, "title");
827            }
828            else
829            {
830                XMLUtils.createElement(contentHandler, "title");
831            }
832
833            _saxTags(tags);
834
835            XMLUtils.endElement(contentHandler, "category");
836        }
837        
838        XMLUtils.endElement(contentHandler, "tag-categories");
839    }
840    
841    /**
842     * Sax a list of tags
843     * @param tags the list of tags to sax
844     * @throws SAXException if an error occurs while saxing
845     */
846    protected void _saxTags(Set<Tag> tags) throws SAXException
847    {
848        XMLUtils.startElement(contentHandler, "tags");
849        for (Tag tag : tags)
850        {
851            AttributesImpl tagAttrs = new AttributesImpl();
852            tagAttrs.addCDATAAttribute("name", tag.getName());
853            XMLUtils.startElement(contentHandler, "tag", tagAttrs);
854
855            tag.getTitle().toSAX(contentHandler);
856
857            XMLUtils.endElement(contentHandler, "tag");
858        }
859        XMLUtils.endElement(contentHandler, "tags");
860    }
861
862}