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