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