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.lang.StringUtils;
046import org.apache.commons.lang.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        Instant dtStamp = event.getDateTimeStamp().map(DtStamp::getDate).orElse(Instant.now());
464        Instant createdAtDate = event.getCreated().map(Created::getDate).orElse(dtStamp);
465        Instant lastModifiedDate = event.getLastModified().map(LastModified::getDate).orElse(dtStamp);
466
467        String createdAt =  org.ametys.core.util.DateUtils.asZonedDateTime(createdAtDate, null).format(DateTimeFormatter.ISO_INSTANT);
468        attrs.addCDATAAttribute("createdAt", createdAt);
469
470        String lastModified =  org.ametys.core.util.DateUtils.asZonedDateTime(lastModifiedDate, null).format(DateTimeFormatter.ISO_INSTANT);
471        attrs.addCDATAAttribute("lastModifiedAt", lastModified);
472        
473        XMLUtils.startElement(handler, "content", attrs);
474
475        XMLUtils.startElement(handler, "metadata");
476        attrs = new AttributesImpl();
477        attrs.addCDATAAttribute("typeId", "string");
478        attrs.addCDATAAttribute("multiple", "false");
479        XMLUtils.createElement(handler, "title", attrs, title);
480        XMLUtils.createElement(handler, "abstract", attrs, eventAbstract);
481        
482
483        attrs = new AttributesImpl();
484        attrs.addCDATAAttribute("typeId", "datetime");
485        attrs.addCDATAAttribute("multiple", "false");
486        XMLUtils.createElement(handler, "start-date", attrs, start);
487        XMLUtils.createElement(handler, "end-date", attrs, end);
488
489        XMLUtils.endElement(handler, "metadata");
490
491        Tag tag = icsEvent.getTag();
492        if (tag != null)
493        {
494            XMLUtils.startElement(handler, "tags");
495            attrs = new AttributesImpl();
496            attrs.addCDATAAttribute("parent", tag.getParentName());
497            XMLUtils.startElement(handler, tag.getName(), attrs);
498            tag.getTitle().toSAX(handler);
499            XMLUtils.endElement(handler, tag.getName());
500            XMLUtils.endElement(handler, "tags");
501        }
502        
503        XMLUtils.endElement(handler, "content");
504        XMLUtils.endElement(handler, "event");
505    }
506
507    /**
508     * SAX information on the months spanning the date range.
509     * @param dateRange the date range.
510     * @throws SAXException if a error occurs while saxing
511     */
512    protected void _saxMonths(EventsFilterHelper.DateTimeRange dateRange) throws SAXException
513    {
514        if (dateRange != null && dateRange.fromDate() != null && dateRange.untilDate() != null)
515        {
516            AttributesImpl atts = new AttributesImpl();
517
518            XMLUtils.startElement(contentHandler, "months");
519
520            ZonedDateTime date = dateRange.fromDate();
521            ZonedDateTime end = dateRange.untilDate();
522
523            while (date.isBefore(end))
524            {
525                int year = date.getYear();
526                int month = date.getMonthValue();
527                
528                String monthStr = String.format("%d-%02d", year, month);
529                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
530
531                atts.clear();
532                atts.addCDATAAttribute("str", monthStr);
533                atts.addCDATAAttribute("raw", dateStr);
534                XMLUtils.startElement(contentHandler, "month", atts);
535
536                XMLUtils.endElement(contentHandler, "month");
537
538                date = date.plusMonths(1);
539            }
540
541            XMLUtils.endElement(contentHandler, "months");
542        }
543    }
544
545    /**
546     * Generate days to build a "calendar" view.
547     * 
548     * @param dateRange a date belonging to the time span to generate.
549     * @param rangeType the range type, "month" or "week".
550     * @throws SAXException if an error occurs while saxing
551     */
552    protected void _saxDaysNew(EventsFilterHelper.DateTimeRange dateRange, String rangeType) throws SAXException
553    {
554        if (dateRange != null)
555        {
556            XMLUtils.startElement(contentHandler, "calendar-months");
557    
558            ZonedDateTime date = dateRange.fromDate();
559            ZonedDateTime end = dateRange.untilDate();
560            
561            while (date.isBefore(end))
562            {
563                int year = date.getYear();
564                int month = date.getMonthValue();
565    
566                String monthStr = String.format("%d-%02d", year, month);
567                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
568    
569                AttributesImpl attrs = new AttributesImpl();
570                attrs.addCDATAAttribute("str", monthStr);
571                attrs.addCDATAAttribute("raw", dateStr);
572                attrs.addCDATAAttribute("year", Integer.toString(year));
573                attrs.addCDATAAttribute("month", Integer.toString(month));
574                XMLUtils.startElement(contentHandler, "month", attrs);
575    
576                _saxDays(date, "month");
577    
578                XMLUtils.endElement(contentHandler, "month");
579    
580                date = date.plusMonths(1);
581            }
582    
583            XMLUtils.endElement(contentHandler, "calendar-months");
584        }
585    }
586
587    /**
588     * Generate days to build a "calendar" view.
589     * 
590     * @param date a date belonging to the time span to generate.
591     * @param type the range type, "month" or "week".
592     * @throws SAXException if an error occurs while saxing
593     */
594    protected void _saxDays(ZonedDateTime date, String type) throws SAXException
595    {
596        AttributesImpl attrs = new AttributesImpl();
597
598        int rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
599        ZonedDateTime previousDay = null;
600        ZonedDateTime nextDay = null;
601
602        // Week.
603        if ("week".equals(type))
604        {
605            rangeStyle = DateUtils.RANGE_WEEK_MONDAY;
606
607            // Get the first day of the week.
608            previousDay = date.with(ChronoField.DAY_OF_WEEK, 1);
609            // First day of next week.
610            nextDay = previousDay.plusWeeks(1);
611            // First day of previous week.
612            previousDay = previousDay.minusWeeks(1);
613        }
614        else
615        {
616            rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
617
618            // Get the first day of the month.
619            previousDay = date.with(ChronoField.DAY_OF_MONTH, 1);
620            // First day of previous month.
621            nextDay = previousDay.plusMonths(1);
622            // First day of next month.
623            previousDay = previousDay.minusMonths(1);
624        }
625
626        addNavAttributes(attrs, date, previousDay, nextDay);
627
628        // Get an iterator on the days to be present on the calendar.
629        
630        Iterator<Calendar> days = DateUtils.iterator(org.ametys.core.util.DateUtils.asDate(date), rangeStyle);
631
632        XMLUtils.startElement(contentHandler, "calendar", attrs);
633
634        ZonedDateTime previousWeekDay = date.minusWeeks(1);
635        ZonedDateTime nextWeekDay = date.plusWeeks(1);
636
637        AttributesImpl weekAttrs = new AttributesImpl();
638        addNavAttributes(weekAttrs, date, previousWeekDay, nextWeekDay);
639
640        XMLUtils.startElement(contentHandler, "week", weekAttrs);
641
642        while (days.hasNext())
643        {
644            Calendar dayCal = days.next();
645            
646            ZonedDateTime day = dayCal.toInstant().atZone(dayCal.getTimeZone().toZoneId());
647            String rawDateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(day);
648            String dateStr = DateTimeFormatter.ISO_LOCAL_DATE.format(day);
649            String yearStr = Integer.toString(dayCal.get(Calendar.YEAR));
650            String monthStr = Integer.toString(dayCal.get(Calendar.MONTH) + 1);
651            String dayStr = Integer.toString(dayCal.get(Calendar.DAY_OF_MONTH));
652
653            AttributesImpl dayAttrs = new AttributesImpl();
654
655            dayAttrs.addCDATAAttribute("raw", rawDateStr);
656            dayAttrs.addCDATAAttribute("date", dateStr);
657            dayAttrs.addCDATAAttribute("year", yearStr);
658            dayAttrs.addCDATAAttribute("month", monthStr);
659            dayAttrs.addCDATAAttribute("day", dayStr);
660
661            XMLUtils.createElement(contentHandler, "day", dayAttrs);
662
663            // Break on week on the last day of the week (but not on the last
664            // week).
665            if (dayCal.get(Calendar.DAY_OF_WEEK) == _eventsFilterHelper.getLastDayOfWeek(dayCal) && days.hasNext())
666            {
667                previousWeekDay = day.minusDays(6);
668                nextWeekDay = day.plusDays(8);
669                
670                weekAttrs.clear();
671                addNavAttributes(weekAttrs, day, previousWeekDay, nextWeekDay);
672
673                XMLUtils.endElement(contentHandler, "week");
674                XMLUtils.startElement(contentHandler, "week", weekAttrs);
675            }
676        }
677
678        XMLUtils.endElement(contentHandler, "week");
679        XMLUtils.endElement(contentHandler, "calendar");
680    }
681
682    /**
683     * Add nav attributes.
684     * 
685     * @param attrs the attributes object to fill in.
686     * @param current the current date.
687     * @param previousDay the previous date.
688     * @param nextDay the next date.
689     */
690    protected void addNavAttributes(AttributesImpl attrs, ZonedDateTime current, ZonedDateTime previousDay, ZonedDateTime nextDay)
691    {
692        attrs.addCDATAAttribute("current", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(current));
693
694        attrs.addCDATAAttribute("previous", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(previousDay));
695        attrs.addCDATAAttribute("previousYear", Integer.toString(previousDay.getYear()));
696        attrs.addCDATAAttribute("previousMonth", Integer.toString(previousDay.getMonthValue()));
697        attrs.addCDATAAttribute("previousDay", Integer.toString(previousDay.getDayOfMonth()));
698
699        attrs.addCDATAAttribute("next", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(nextDay));
700        attrs.addCDATAAttribute("nextYear", Integer.toString(nextDay.getYear()));
701        attrs.addCDATAAttribute("nextMonth", Integer.toString(nextDay.getMonthValue()));
702        attrs.addCDATAAttribute("nextDay", Integer.toString(nextDay.getDayOfMonth()));
703    }
704
705    /**
706     * Generate the list of selected tags.
707     * @param tags the list of tags.
708     * @throws SAXException if an error occurs while saxing
709     */
710    protected void _saxTags(Collection<String> tags) throws SAXException
711    {
712        XMLUtils.startElement(contentHandler, "tags");
713        for (String tag : tags)
714        {
715            AttributesImpl attrs = new AttributesImpl();
716            attrs.addCDATAAttribute("name", tag);
717            XMLUtils.createElement(contentHandler, "tag", attrs);
718        }
719        XMLUtils.endElement(contentHandler, "tags");
720    }
721
722    /**
723     * Generate the list of selected tags that act as categories and their descendant tags.
724     * @param categories the list of categories to generate.
725     * @param icsTags list of tags for the ICS feeds (tags, not parents)
726     * @throws SAXException if an error occurs while saxing
727     */
728    protected void _saxCategories(Collection<Tag> categories, Collection<Tag> icsTags) throws SAXException
729    {
730        Map<Tag, Set<Tag>> icsTagsToAdd = new HashMap<>();
731        
732        // Only the tags that are not already in the ones from the search contexts
733        for (Tag tag : icsTags)
734        {
735            Tag parent = tag.getParent();
736            if (icsTagsToAdd.containsKey(parent))
737            {
738                icsTagsToAdd.get(parent).add(tag);
739            }
740            else if (categories == null || !categories.contains(tag.getParent()))
741            {
742                icsTagsToAdd.put(parent, new LinkedHashSet<>());
743                icsTagsToAdd.get(parent).add(tag);
744            }
745        }
746        
747        XMLUtils.startElement(contentHandler, "tag-categories");
748        
749     // Add the tags from the search contexts
750        if (categories != null)
751        {
752            for (Tag category : categories)
753            {
754                XMLUtils.startElement(contentHandler, "category");
755    
756                category.getTitle().toSAX(contentHandler, "title");
757    
758                _saxTags(_eventsFilterHelper.getAllTags(category));
759    
760                XMLUtils.endElement(contentHandler, "category");
761            }
762        }
763        
764        // Add the tags from the ICS feeds
765        for (Entry<Tag, Set<Tag>> entry : icsTagsToAdd.entrySet())
766        {
767            Tag parent = entry.getKey();
768            Set<Tag> tags = entry.getValue();
769            
770            XMLUtils.startElement(contentHandler, "category");
771
772            // As in the ICS, we select directly a tag and not a category, it is possible that there are no parent.
773            // To keep the XML equivalent, the category is still created, possibly with an empty title
774            if (parent != null)
775            {
776                parent.getTitle().toSAX(contentHandler, "title");
777            }
778            else
779            {
780                XMLUtils.createElement(contentHandler, "title");
781            }
782
783            _saxTags(tags);
784
785            XMLUtils.endElement(contentHandler, "category");
786        }
787        
788        XMLUtils.endElement(contentHandler, "tag-categories");
789    }
790    
791    /**
792     * Sax a list of tags
793     * @param tags the list of tags to sax
794     * @throws SAXException if an error occurs while saxing
795     */
796    protected void _saxTags(Set<Tag> tags) throws SAXException
797    {
798        XMLUtils.startElement(contentHandler, "tags");
799        for (Tag tag : tags)
800        {
801            AttributesImpl tagAttrs = new AttributesImpl();
802            tagAttrs.addCDATAAttribute("name", tag.getName());
803            XMLUtils.startElement(contentHandler, "tag", tagAttrs);
804
805            tag.getTitle().toSAX(contentHandler);
806
807            XMLUtils.endElement(contentHandler, "tag");
808        }
809        XMLUtils.endElement(contentHandler, "tags");
810    }
811
812}