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