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.net.URLDecoder;
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.Calendar;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Date;
029import java.util.Iterator;
030import java.util.Map;
031import java.util.Set;
032
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.cocoon.ProcessingException;
036import org.apache.cocoon.environment.ObjectModelHelper;
037import org.apache.cocoon.environment.Request;
038import org.apache.cocoon.xml.AttributesImpl;
039import org.apache.cocoon.xml.XMLUtils;
040import org.apache.commons.lang.StringUtils;
041import org.apache.commons.lang.time.DateUtils;
042import org.xml.sax.ContentHandler;
043import org.xml.sax.SAXException;
044
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.tag.Tag;
047import org.ametys.plugins.repository.AmetysObjectIterable;
048import org.ametys.plugins.repository.AmetysObjectResolver;
049import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
050import org.ametys.web.WebConstants;
051import org.ametys.web.filter.WebContentFilter;
052import org.ametys.web.filter.WebContentFilter.AccessLimitation;
053import org.ametys.web.repository.page.Page;
054import org.ametys.web.repository.page.ZoneItem;
055
056/**
057 * Query and generate news according to many parameters.
058 */
059public class EventsGenerator extends AbstractEventGenerator
060{
061    /** The ametys object resolver. */
062    protected AmetysObjectResolver _ametysResolver;
063
064    /** The events helper */
065    protected EventsFilterHelper _eventsFilterHelper;
066
067
068    @Override
069    public void service(ServiceManager serviceManager) throws ServiceException
070    {
071        super.service(serviceManager);
072        _ametysResolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
073        _eventsFilterHelper = (EventsFilterHelper) serviceManager.lookup(EventsFilterHelper.ROLE);
074    }
075
076    @Override
077    public void generate() throws IOException, SAXException, ProcessingException
078    {
079        Request request = ObjectModelHelper.getRequest(objectModel);
080        @SuppressWarnings("unchecked")
081        Map<String, Object> parentContextAttrs = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
082        if (parentContextAttrs == null)
083        {
084            parentContextAttrs = Collections.EMPTY_MAP;
085        }
086
087        LocalDate today = LocalDate.now();
088
089        // Get site and language in sitemap parameters. Can not be null.
090        String siteName = parameters.getParameter("site", (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME));
091        String lang = parameters.getParameter("lang", (String) request.getAttribute("renderingLanguage"));
092        if (StringUtils.isEmpty(lang))
093        {
094            lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
095        }
096        // Get the parameters.
097        int monthsBefore = parameters.getParameterAsInteger("months-before", 3);
098        int monthsAfter = parameters.getParameterAsInteger("months-after", 3);
099        // Type can be "calendar", "single-day" or "agenda".
100        String type = parameters.getParameter("type", "calendar");
101        String view = parameters.getParameter("view", "");
102        int year = parameters.getParameterAsInteger("year", today.getYear());
103        int month = parameters.getParameterAsInteger("month", today.getMonthValue());
104        int day = parameters.getParameterAsInteger("day", today.getDayOfMonth());
105        // Select a single tag or "all".
106        String requestedTagsString = parameters.getParameter("tags", "all");
107        
108        
109        Page page = (Page) request.getAttribute(WebConstants.REQUEST_ATTR_PAGE);
110        // Get the zone item, as a request attribute or from the ID in the
111        // parameters.
112        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
113        String zoneItemId = parameters.getParameter("zoneItemId", "");
114        if (zoneItem == null && StringUtils.isNotEmpty(zoneItemId))
115        {
116            zoneItemId = URLDecoder.decode(zoneItemId, "UTF-8");
117            zoneItem = (ZoneItem) _ametysResolver.resolveById(zoneItemId);
118        }
119        
120        if (page == null && zoneItem != null)
121        {
122            // Wrapped page such as _plugins/calendar/page/YEAR/MONTH/DAY/ZONEITEMID/events_1.3.html => get the page from its zone item
123            // The page is needed to get restriction
124            page = zoneItem.getZone().getPage();
125        }
126        
127        ZonedDateTime dateTime = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, ZoneId.systemDefault());
128        String title = _eventsFilterHelper.getTitle(zoneItem);
129        String rangeType = parameters.getParameter("rangeType", _eventsFilterHelper.getDefaultRangeType(zoneItem));
130        boolean maskOrphan = _eventsFilterHelper.getMaskOrphan(zoneItem);
131        boolean pdfDownload = _eventsFilterHelper.getPdfDownload(zoneItem);
132        boolean icalDownload = _eventsFilterHelper.getIcalDownload(zoneItem);
133        String link = _eventsFilterHelper.getLink(zoneItem);
134        String linkTitle = _eventsFilterHelper.getLinkTitle(zoneItem);
135        
136        boolean doRetrieveView = !StringUtils.equalsIgnoreCase("false", parameters.getParameter("do-retrieve-view", "true"));
137
138        // Get the tags to match, from the zone item or from the parameters.
139        String[] tagsArray = (String[]) parentContextAttrs.get("tags");
140        Set<String> tags = _eventsFilterHelper.getTags(zoneItem, tagsArray);
141        // Get the categories of the tags to match, from the zone item or from
142        // the parameters.
143        String[] categoriesArray = (String[]) parentContextAttrs.get("tag-categories");
144        Set<Tag> categories = _eventsFilterHelper.getTagCategories(zoneItem, siteName, categoriesArray);
145        String pagePath = page != null ? page.getPathInSitemap() : "";
146        
147        Set<String> filteredCategories = _eventsFilterHelper.getFilteredCategories(categoriesArray, requestedTagsString.split(","), zoneItem, siteName);
148        // Get the date range and deduce the expression (single day or month-before to month-after).
149        EventsFilterHelper.DateRange dateRange = _eventsFilterHelper.getDateRange(type, year, month, day, monthsBefore, monthsAfter, rangeType);
150        // Get the corresponding contents.
151        EventsFilter eventsFilter = _eventsFilterHelper.generateEventFilter(dateRange, zoneItem, view, type, tagsArray, filteredCategories);
152        AmetysObjectIterable<Content> eventContents = eventsFilter.getMatchingContents(siteName, lang, page);
153
154        _sax(today, monthsBefore, monthsAfter, year, month, day, filteredCategories, page, zoneItem, dateTime, title, rangeType, maskOrphan, pdfDownload, icalDownload, link, linkTitle, doRetrieveView, tags, categories, pagePath, eventsFilter, dateRange, eventContents);
155    }
156
157    private void _sax(LocalDate today, int monthsBefore, int monthsAfter, int year, int month, int day, Set<String> filteredCategoryTags, Page page, ZoneItem zoneItem, ZonedDateTime dateTime,
158            String title, String rangeType, boolean maskOrphan, boolean pdfDownload, boolean icalDownload, String link, String linkTitle, boolean doRetrieveView, Set<String> tags,
159            Set<Tag> categories, String pagePath, EventsFilter eventsFilter, EventsFilterHelper.DateRange dateRange, AmetysObjectIterable<Content> eventContents)
160            throws SAXException, IOException
161    {
162        AttributesImpl atts = new AttributesImpl();
163
164        atts.addCDATAAttribute("page-path", pagePath);
165        atts.addCDATAAttribute("today", DateTimeFormatter.ISO_LOCAL_DATE.format(today));
166        if (dateRange != null)
167        {
168            if (dateRange.getStartDate() != null)
169            {
170                atts.addCDATAAttribute("start", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault())));
171            }
172            if (dateRange.getEndDate() != null)
173            {
174                atts.addCDATAAttribute("end", DateTimeFormatter.ISO_LOCAL_DATE.format(dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault())));
175            }
176        }
177
178        atts.addCDATAAttribute("year", Integer.toString(year));
179        atts.addCDATAAttribute("month", String.format("%02d", month));
180        atts.addCDATAAttribute("day", String.format("%02d", day));
181        atts.addCDATAAttribute("months-before", Integer.toString(monthsBefore));
182        atts.addCDATAAttribute("months-after", Integer.toString(monthsAfter));
183
184        atts.addCDATAAttribute("title", title);
185        atts.addCDATAAttribute("mask-orphan", Boolean.toString(maskOrphan));
186        atts.addCDATAAttribute("pdf-download", Boolean.toString(pdfDownload));
187        atts.addCDATAAttribute("ical-download", Boolean.toString(icalDownload));
188        atts.addCDATAAttribute("link", link);
189        atts.addCDATAAttribute("link-title", linkTitle);
190
191        if (zoneItem != null)
192        {
193            atts.addCDATAAttribute("zoneItemId", zoneItem.getId());
194        }
195        if (StringUtils.isNotEmpty(rangeType))
196        {
197            atts.addCDATAAttribute("range", rangeType);
198        }
199        
200        if (!filteredCategoryTags.isEmpty())
201        {
202            atts.addCDATAAttribute("requested-tags", String.join(",", filteredCategoryTags));
203        }
204
205        contentHandler.startDocument();
206        XMLUtils.startElement(contentHandler, "events", atts);
207
208        _saxRssUrl(zoneItem);
209
210        // Generate months (used in calendar mode) and days (used in full-page
211        // agenda mode).
212        _saxMonths(dateRange);
213        _saxDays(dateTime, rangeType);
214
215        _saxDaysNew(dateRange, rangeType);
216
217        // Generate tags and categories.
218        _saxTags(tags);
219        _saxCategories(categories);
220
221        // Generate the matching contents.
222        XMLUtils.startElement(contentHandler, "contents");
223
224        saxMatchingContents(contentHandler, eventsFilter, eventContents, page, doRetrieveView);
225
226        XMLUtils.endElement(contentHandler, "contents");
227
228        XMLUtils.endElement(contentHandler, "events");
229        contentHandler.endDocument();
230    }
231
232    private void _saxRssUrl(ZoneItem zoneItem) throws SAXException 
233    {
234        if (zoneItem != null)
235        {
236            ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
237            // First check that there is a value because calendar service doesn't define the rss parameter
238            if (serviceParameters.hasValue("rss") && (boolean) serviceParameters.getValue("rss"))
239            {
240                // Split protocol and id
241                String[] zoneItemId = zoneItem.getId().split("://");
242                String url = "_plugins/calendar/" + zoneItemId[1] + "/rss.xml";
243                
244                XMLUtils.createElement(contentHandler, "rssUrl", url);
245            }
246        }
247    }
248    
249    /**
250     * SAX all contents matching the given filter
251     * 
252     * @param handler The content handler to SAX into
253     * @param filter The filter
254     * @param contents iterator on the contents.
255     * @param currentPage The current page.
256     * @param saxContentItSelf true to sax the content, false will only sax some meta
257     * @throws SAXException If an error occurs while SAXing
258     * @throws IOException If an error occurs while retrieving content.
259     */
260    public void saxMatchingContents(ContentHandler handler, WebContentFilter filter, AmetysObjectIterable<Content> contents, Page currentPage, boolean saxContentItSelf) throws SAXException, IOException
261    {
262        boolean checkUserAccess = filter.getAccessLimitation() == AccessLimitation.USER_ACCESS;
263        
264        for (Content content : contents)
265        {
266            if (_filterHelper.isContentValid(content, currentPage, filter))
267            {
268                saxContent(handler, content, saxContentItSelf, filter, checkUserAccess);
269            }
270        }
271    }
272
273    /**
274     * SAX information on the months spanning the date range.
275     * @param dateRange the date range.
276     * @throws SAXException if a error occurs while saxing
277     */
278    protected void _saxMonths(EventsFilterHelper.DateRange dateRange) throws SAXException
279    {
280        if (dateRange != null && dateRange.getStartDate() != null && dateRange.getEndDate() != null)
281        {
282            AttributesImpl atts = new AttributesImpl();
283
284            XMLUtils.startElement(contentHandler, "months");
285
286            ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault());
287            ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault());
288
289            while (date.isBefore(end))
290            {
291                int year = date.getYear();
292                int month = date.getMonthValue();
293
294                String monthStr = String.format("%d-%02d", year, month);
295                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
296
297                atts.clear();
298                atts.addCDATAAttribute("str", monthStr);
299                atts.addCDATAAttribute("raw", dateStr);
300                XMLUtils.startElement(contentHandler, "month", atts);
301
302                XMLUtils.endElement(contentHandler, "month");
303
304                date = date.plusMonths(1);
305            }
306
307            XMLUtils.endElement(contentHandler, "months");
308        }
309    }
310
311    /**
312     * Generate days to build a "calendar" view.
313     * 
314     * @param dateRange a date belonging to the time span to generate.
315     * @param rangeType the range type, "month" or "week".
316     * @throws SAXException if an error occurs while saxing
317     */
318    protected void _saxDaysNew(EventsFilterHelper.DateRange dateRange, String rangeType) throws SAXException
319    {
320        if (dateRange != null)
321        {
322            XMLUtils.startElement(contentHandler, "calendar-months");
323    
324            ZonedDateTime date = dateRange.getStartDate().toInstant().atZone(ZoneId.systemDefault());
325            ZonedDateTime end = dateRange.getEndDate().toInstant().atZone(ZoneId.systemDefault());
326            
327    
328            while (date.isBefore(end))
329            {
330                int year = date.getYear();
331                int month = date.getMonthValue();
332    
333                String monthStr = String.format("%d-%02d", year, month);
334                String dateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(date);
335    
336                AttributesImpl attrs = new AttributesImpl();
337                attrs.addCDATAAttribute("str", monthStr);
338                attrs.addCDATAAttribute("raw", dateStr);
339                attrs.addCDATAAttribute("year", Integer.toString(year));
340                attrs.addCDATAAttribute("month", Integer.toString(month));
341                XMLUtils.startElement(contentHandler, "month", attrs);
342    
343                _saxDays(date, "month");
344    
345                XMLUtils.endElement(contentHandler, "month");
346    
347                date = date.plusMonths(1);
348            }
349    
350            XMLUtils.endElement(contentHandler, "calendar-months");
351        }
352    }
353
354    /**
355     * Generate days to build a "calendar" view.
356     * 
357     * @param date a date belonging to the time span to generate.
358     * @param type the range type, "month" or "week".
359     * @throws SAXException if an error occurs while saxing
360     */
361    protected void _saxDays(ZonedDateTime date, String type) throws SAXException
362    {
363        AttributesImpl attrs = new AttributesImpl();
364
365        int rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
366        ZonedDateTime previousDay = null;
367        ZonedDateTime nextDay = null;
368
369        // Week.
370        if ("week".equals(type))
371        {
372            rangeStyle = DateUtils.RANGE_WEEK_MONDAY;
373
374            // Get the first day of the week.
375            previousDay = date.with(ChronoField.DAY_OF_WEEK, 1);
376            // First day of next week.
377            nextDay = previousDay.plusWeeks(1);
378            // First day of previous week.
379            previousDay = previousDay.minusWeeks(1);
380        }
381        else
382        {
383            rangeStyle = DateUtils.RANGE_MONTH_MONDAY;
384
385            // Get the first day of the month.
386            previousDay = date.with(ChronoField.DAY_OF_MONTH, 1);
387            // First day of previous month.
388            nextDay = previousDay.plusMonths(1);
389            // First day of next month.
390            previousDay = previousDay.minusMonths(1);
391        }
392
393        addNavAttributes(attrs, date, previousDay, nextDay);
394
395        // Get an iterator on the days to be present on the calendar.
396        
397        Iterator<Calendar> days = DateUtils.iterator(Date.from(date.toInstant()), rangeStyle);
398
399        XMLUtils.startElement(contentHandler, "calendar", attrs);
400
401        ZonedDateTime previousWeekDay = date.minusWeeks(1);
402        ZonedDateTime nextWeekDay = date.plusWeeks(1);
403
404        AttributesImpl weekAttrs = new AttributesImpl();
405        addNavAttributes(weekAttrs, date, previousWeekDay, nextWeekDay);
406
407        XMLUtils.startElement(contentHandler, "week", weekAttrs);
408
409        while (days.hasNext())
410        {
411            Calendar dayCal = days.next();
412            ZonedDateTime day = dayCal.toInstant().atZone(dayCal.getTimeZone().toZoneId());
413            String rawDateStr = org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(day);
414            String dateStr = DateTimeFormatter.ISO_LOCAL_DATE.format(day);
415            String yearStr = Integer.toString(dayCal.get(Calendar.YEAR));
416            String monthStr = Integer.toString(dayCal.get(Calendar.MONTH) + 1);
417            String dayStr = Integer.toString(dayCal.get(Calendar.DAY_OF_MONTH));
418
419            AttributesImpl dayAttrs = new AttributesImpl();
420
421            dayAttrs.addCDATAAttribute("raw", rawDateStr);
422            dayAttrs.addCDATAAttribute("date", dateStr);
423            dayAttrs.addCDATAAttribute("year", yearStr);
424            dayAttrs.addCDATAAttribute("month", monthStr);
425            dayAttrs.addCDATAAttribute("day", dayStr);
426
427            XMLUtils.createElement(contentHandler, "day", dayAttrs);
428
429            // Break on week on the last day of the week (but not on the last
430            // week).
431            if (dayCal.get(Calendar.DAY_OF_WEEK) == _eventsFilterHelper.getLastDayOfWeek(dayCal) && days.hasNext())
432            {
433                previousWeekDay = day.minusDays(6);
434                nextWeekDay = day.plusDays(8);
435                weekAttrs.clear();
436
437                addNavAttributes(weekAttrs, day, previousWeekDay, nextWeekDay);
438
439                XMLUtils.endElement(contentHandler, "week");
440                XMLUtils.startElement(contentHandler, "week", weekAttrs);
441            }
442        }
443
444        XMLUtils.endElement(contentHandler, "week");
445        XMLUtils.endElement(contentHandler, "calendar");
446    }
447
448    /**
449     * Add nav attributes.
450     * 
451     * @param attrs the attributes object to fill in.
452     * @param current the current date.
453     * @param previousDay the previous date.
454     * @param nextDay the next date.
455     */
456    protected void addNavAttributes(AttributesImpl attrs, ZonedDateTime current, ZonedDateTime previousDay, ZonedDateTime nextDay)
457    {
458        attrs.addCDATAAttribute("current", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(current));
459
460        attrs.addCDATAAttribute("previous", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(previousDay));
461        attrs.addCDATAAttribute("previousYear", Integer.toString(previousDay.getYear()));
462        attrs.addCDATAAttribute("previousMonth", Integer.toString(previousDay.getMonthValue()));
463        attrs.addCDATAAttribute("previousDay", Integer.toString(previousDay.getDayOfMonth()));
464
465        attrs.addCDATAAttribute("next", org.ametys.core.util.DateUtils.getISODateTimeFormatter().format(nextDay));
466        attrs.addCDATAAttribute("nextYear", Integer.toString(nextDay.getYear()));
467        attrs.addCDATAAttribute("nextMonth", Integer.toString(nextDay.getMonthValue()));
468        attrs.addCDATAAttribute("nextDay", Integer.toString(nextDay.getDayOfMonth()));
469    }
470
471    /**
472     * Generate the list of selected tags.
473     * @param tags the list of tags.
474     * @throws SAXException if an error occurs while saxing
475     */
476    protected void _saxTags(Collection<String> tags) throws SAXException
477    {
478        XMLUtils.startElement(contentHandler, "tags");
479        for (String tag : tags)
480        {
481            AttributesImpl attrs = new AttributesImpl();
482            attrs.addCDATAAttribute("name", tag);
483            XMLUtils.createElement(contentHandler, "tag", attrs);
484        }
485        XMLUtils.endElement(contentHandler, "tags");
486    }
487
488    /**
489     * Generate the list of selected tags that act as categories and their descendant tags.
490     * @param categories the list of categories to generate.
491     * @throws SAXException if an error occurs while saxing
492     */
493    protected void _saxCategories(Collection<Tag> categories) throws SAXException
494    {
495        XMLUtils.startElement(contentHandler, "tag-categories");
496        for (Tag category : categories)
497        {
498            AttributesImpl categoryAttrs = new AttributesImpl();
499
500            XMLUtils.startElement(contentHandler, "category", categoryAttrs);
501
502            category.getTitle().toSAX(contentHandler, "title");
503
504            XMLUtils.startElement(contentHandler, "tags");
505            for (Tag tag : _eventsFilterHelper.getAllTags(category))
506            {
507                AttributesImpl tagAttrs = new AttributesImpl();
508                tagAttrs.addCDATAAttribute("name", tag.getName());
509                XMLUtils.startElement(contentHandler, "tag", tagAttrs);
510
511                tag.getTitle().toSAX(contentHandler);
512
513                XMLUtils.endElement(contentHandler, "tag");
514            }
515            XMLUtils.endElement(contentHandler, "tags");
516
517            XMLUtils.endElement(contentHandler, "category");
518        }
519        XMLUtils.endElement(contentHandler, "tag-categories");
520    }
521
522
523
524}