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