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