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