001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.util;
029
030import org.opencms.acacia.shared.I_CmsSerialDateValue;
031import org.opencms.acacia.shared.I_CmsSerialDateValue.DateType;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsResource;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.search.galleries.CmsGallerySearch;
037import org.opencms.search.galleries.CmsGallerySearchResult;
038import org.opencms.util.CmsCollectionsGenericWrapper;
039import org.opencms.widgets.serialdate.CmsSerialDateBeanFactory;
040import org.opencms.widgets.serialdate.CmsSerialDateValue;
041import org.opencms.widgets.serialdate.I_CmsSerialDateBean;
042
043import java.util.ArrayList;
044import java.util.Calendar;
045import java.util.Date;
046import java.util.GregorianCalendar;
047import java.util.List;
048import java.util.Locale;
049import java.util.Map;
050import java.util.SortedSet;
051import java.util.TreeSet;
052
053import org.apache.commons.collections.Transformer;
054import org.apache.commons.logging.Log;
055
056import com.google.common.base.Objects;
057
058/** Bean for easy access to information of an event series. */
059public class CmsJspDateSeriesBean {
060
061    /**
062     * Provides information on the single event when the start date is provided.<p>
063     *
064     * If no valid start date is provided, the information for the first event in the series is provided.
065     */
066    public class CmsSeriesSingleEventTransformer implements Transformer {
067
068        /**
069         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
070         */
071        public Object transform(Object date) {
072
073            Date d = toDate(date);
074            if ((null != d) && m_dates.contains(d)) {
075                return new CmsJspInstanceDateBean((Date)d.clone(), CmsJspDateSeriesBean.this);
076            } else {
077                if (!m_dates.isEmpty()) {
078                    return getFirst();
079                }
080            }
081            return null;
082        }
083    }
084
085    /** Logger for the class. */
086    private static final Log LOG = CmsLog.getLog(CmsJspDateSeriesBean.class);
087
088    /** The duration of a single event. */
089    private Long m_duration;
090
091    /** The first event of the series. */
092    private CmsJspInstanceDateBean m_firstEvent;
093
094    /** Flag, indicating if the single events last over night. */
095    private Boolean m_isMultiDay;
096
097    /** The last event of the series. */
098    private CmsJspInstanceDateBean m_lastEvent;
099
100    /** The locale to use for displaying dates. */
101    private Locale m_locale;
102
103    /** The parent series. */
104    private CmsJspDateSeriesBean m_parentSeries;
105
106    /** The series definition. */
107    private I_CmsSerialDateValue m_seriesDefinition;
108
109    /** Lazy map from start dates (provided as Long, long value as string or date) to informations on the single event. */
110    private Map<Object, CmsJspInstanceDateBean> m_singleEvents;
111
112    /** The content value containing the series definition. */
113    private CmsJspContentAccessValueWrapper m_value;
114
115    /** The dates of the series. */
116    SortedSet<Date> m_dates;
117
118    /**
119     * Constructor for a date series bean.<p>
120     *
121     * @param value the content value wrapper for the element that stores the series definition
122     * @param locale the locale in which dates should be rendered. This can differ from the content locale, if e.g.
123     *          on a German page a content that is only present in English is rendered.
124     */
125    public CmsJspDateSeriesBean(CmsJspContentAccessValueWrapper value, Locale locale) {
126
127        m_value = value;
128        m_locale = null == locale ? m_value.getLocale() : locale;
129        initFromSeriesDefinition(value.toString());
130    }
131
132    /**
133     * Constructor for the date series bean for testing purposes where no OpenCms object is required.<p>
134     *
135     * NOTE: The parent series cannot be determined if this constructor is used.<p>
136     *
137     * @param seriesDefinition the series definition.
138     * @param locale the locale in which dates should be rendered. This can differ from the content locale, if e.g.
139     *          on a German page a content that is only present in English is rendered.
140     */
141    CmsJspDateSeriesBean(String seriesDefinition, Locale locale) {
142
143        m_locale = locale;
144        initFromSeriesDefinition(seriesDefinition);
145    }
146
147    /**
148     * Returns the list of start dates for all instances of the series.<p>
149     *
150     * @return the list of start dates for all instances of the series
151     */
152    public List<Date> getDates() {
153
154        return new ArrayList<>(m_dates);
155    }
156
157    /**
158     * Returns the first event of this series.<p>
159     *
160     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
161     *
162     * @return the first event of this series
163     */
164    public CmsJspInstanceDateBean getFirst() {
165
166        if ((m_firstEvent == null) && (m_dates != null) && (!m_dates.isEmpty())) {
167            m_firstEvent = new CmsJspInstanceDateBean((Date)m_dates.first().clone(), CmsJspDateSeriesBean.this);
168        }
169        return m_firstEvent;
170    }
171
172    /**
173     * Returns the duration of a single instance in milliseconds.<p>
174     *
175     * @return the duration of a single instance in milliseconds
176     */
177    public Long getInstanceDuration() {
178
179        return m_duration;
180    }
181
182    /**
183     * Returns a lazy map from the start time of a single instance of the series to the date information on the single instance.<p>
184     *
185     * Start time can be provided as Long, as a String representation of the long value or as Date.<p>
186     *
187     * If no event exists for the start time, the information for the first event of the series is returned.
188     *
189     * @return a lazy map from the start time of a single instance of the series to the date information on the single instance.
190     */
191    public Map<Object, CmsJspInstanceDateBean> getInstanceInfo() {
192
193        if (m_singleEvents == null) {
194            m_singleEvents = CmsCollectionsGenericWrapper.createLazyMap(new CmsSeriesSingleEventTransformer());
195        }
196        return m_singleEvents;
197    }
198
199    /**
200     * Returns a flag, indicating if the series is extracted from another series.<p>
201     *
202     * @return a flag, indicating if the series is extracted from another series
203     */
204    public boolean getIsExtractedDate() {
205
206        return Objects.equal(m_seriesDefinition.getDateType(), DateType.EXTRACTED);
207    }
208
209    /**
210     * Returns a flag, indicating if the series is defined via a pattern, i.e., not just as via single date.<p>
211     *
212     * @return a flag, indicating if the series is defined via a pattern, i.e., not just as via single date
213     */
214    public boolean getIsSeries() {
215
216        return Objects.equal(m_seriesDefinition.getDateType(), DateType.SERIES);
217    }
218
219    /**
220     * Returns a flag, indicating if the series is defined by only a single date and not extracted from another series.<p>
221     *
222     * @return a flag, indicating if the series is defined by only a single date and not extracted from another series
223     */
224    public boolean getIsSingleDate() {
225
226        return Objects.equal(m_seriesDefinition.getDateType(), DateType.SINGLE);
227    }
228
229    /**
230     * Returns the last event of this series.<p>
231     *
232     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
233     *
234     * @return the last event of this series
235     */
236    public CmsJspInstanceDateBean getLast() {
237
238        if ((m_lastEvent == null) && (m_dates != null) && (!m_dates.isEmpty())) {
239            m_lastEvent = new CmsJspInstanceDateBean((Date)m_dates.last().clone(), CmsJspDateSeriesBean.this);
240        }
241        return m_lastEvent;
242    }
243
244    /**
245     * Returns the locale to use for rendering dates.<p>
246     *
247     * @return the locale to use for rendering dates
248     */
249    public Locale getLocale() {
250
251        return m_locale;
252    }
253
254    /**
255     * Returns the next event of this series relative to the current date.<p>
256     *
257     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
258     *
259     * @return the next event of this series
260     */
261    public CmsJspInstanceDateBean getNext() {
262
263        return getNextFor(new Date());
264    }
265
266    /**
267     * Returns the next event of this series that takes place at the given date or after it.<p>
268     *
269     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
270     *
271     *@param date the date relative to which the event is returned.
272     *
273     * @return the next event of this series
274     */
275    public CmsJspInstanceDateBean getNextFor(Object date) {
276
277        Date d = toDate(date);
278        if ((d != null) && (m_dates != null) && (!m_dates.isEmpty())) {
279            for (Date instanceDate : m_dates) {
280                if (!instanceDate.before(d)) {
281                    return new CmsJspInstanceDateBean((Date)instanceDate.clone(), CmsJspDateSeriesBean.this);
282                }
283            }
284        }
285        return getLast();
286    }
287
288    /**
289     * Returns the parent series, if it is present, otherwise <code>null</code>.<p>
290     *
291     * @return the parent series, if it is present, otherwise <code>null</code>
292     */
293    public CmsJspDateSeriesBean getParentSeries() {
294
295        if ((m_parentSeries == null) && getIsExtractedDate()) {
296            CmsObject cms = m_value.getCmsObject();
297            try {
298                CmsResource res = cms.readResource(m_seriesDefinition.getParentSeriesId());
299                CmsJspContentAccessBean content = new CmsJspContentAccessBean(cms, m_value.getLocale(), res);
300                CmsJspContentAccessValueWrapper value = content.getValue().get(m_value.getPath());
301                return new CmsJspDateSeriesBean(value, m_locale);
302            } catch (NullPointerException | CmsException e) {
303                LOG.warn("Parent series with id " + m_seriesDefinition.getParentSeriesId() + " could not be read.", e);
304            }
305
306        }
307        return null;
308    }
309
310    /**
311     * Returns the next event of this series relative to the current date.<p>
312     *
313     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
314     *
315     * @return the next event of this series
316     */
317    public CmsJspInstanceDateBean getPrevious() {
318
319        return getPreviousFor(new Date());
320    }
321
322    /**
323     * Returns the next event of this series that takes place at the given date or after it.<p>
324     *
325     * In case this is just a single event and not a series, this is identical to the date of the event.<p>
326     *
327     *@param date the date relative to which the event is returned.
328     *
329     * @return the next event of this series
330     */
331    public CmsJspInstanceDateBean getPreviousFor(Object date) {
332
333        Date d = toDate(date);
334        if ((d != null) && (m_dates != null) && (!m_dates.isEmpty())) {
335            Date lastDate = m_dates.first();
336            for (Date instanceDate : m_dates) {
337                if (instanceDate.after(d)) {
338                    return new CmsJspInstanceDateBean((Date)lastDate.clone(), CmsJspDateSeriesBean.this);
339                }
340                lastDate = instanceDate;
341            }
342            return new CmsJspInstanceDateBean((Date)lastDate.clone(), CmsJspDateSeriesBean.this);
343        }
344        return getLast();
345    }
346
347    /**
348     * Returns the gallery title of the series content.<p>
349     *
350     * @return the gallery title of the series content
351     */
352    public String getTitle() {
353
354        CmsGallerySearchResult result;
355        try {
356            result = CmsGallerySearch.searchById(
357                m_value.getCmsObject(),
358                m_value.getContentValue().getDocument().getFile().getStructureId(),
359                m_value.getLocale());
360            return result.getTitle();
361        } catch (CmsException e) {
362            LOG.warn("Could not retrieve title of series content.", e);
363            return "";
364        }
365
366    }
367
368    /**
369     * Returns a flag, indicating if the single events last over night.<p>
370     *
371     * @return <code>true</code> if the event ends on another day than it starts, <code>false</code> if it ends on the same day
372     */
373    public boolean isMultiDay() {
374
375        if (m_isMultiDay != null) {
376            return m_isMultiDay.booleanValue();
377        }
378
379        Long instanceDuration = getInstanceDuration();
380
381        if ((null == instanceDuration) || (instanceDuration.longValue() == 0)) {
382            m_isMultiDay = Boolean.FALSE;
383            return false;
384        }
385        if (getInstanceDuration().longValue() > I_CmsSerialDateValue.DAY_IN_MILLIS) {
386            m_isMultiDay = Boolean.TRUE;
387            return true;
388        }
389        if (isWholeDay() && (getInstanceDuration().longValue() <= I_CmsSerialDateValue.DAY_IN_MILLIS)) {
390            m_isMultiDay = Boolean.FALSE;
391            return false;
392        }
393        Calendar start = new GregorianCalendar();
394        start.setTime(m_seriesDefinition.getStart());
395        Calendar end = new GregorianCalendar();
396        end.setTime(m_seriesDefinition.getEnd());
397        if (start.get(Calendar.DAY_OF_MONTH) == end.get(Calendar.DAY_OF_MONTH)) {
398            m_isMultiDay = Boolean.FALSE;
399            return false;
400        }
401        m_isMultiDay = Boolean.TRUE;
402        return true;
403    }
404
405    /**
406     * Returns a flag, indicating if the events in the series last whole days.<p>
407     *
408     * @return a flag, indicating if the events in the series last whole days
409     */
410    public boolean isWholeDay() {
411
412        return m_seriesDefinition.isWholeDay();
413    }
414
415    /**
416     * Converts the provided object to a date, if possible.
417     *
418     * @param date the date.
419     *
420     * @return the date as {@link java.util.Date}
421     */
422    public Date toDate(Object date) {
423
424        Date d = null;
425        if (null != date) {
426            if (date instanceof Date) {
427                d = (Date)date;
428            } else if (date instanceof Long) {
429                d = new Date(((Long)date).longValue());
430            } else {
431                try {
432                    long l = Long.parseLong(date.toString());
433                    d = new Date(l);
434                } catch (Exception e) {
435                    // do nothing, just let d remain null
436                }
437            }
438        }
439        return d;
440    }
441
442    /**
443     * Initialization based on the series definition.<p>
444     *
445     * @param seriesDefinition the series definition
446     */
447    private void initFromSeriesDefinition(String seriesDefinition) {
448
449        String seriesDefinitionString = seriesDefinition;
450        m_seriesDefinition = new CmsSerialDateValue(seriesDefinitionString);
451        if (m_seriesDefinition.isValid()) {
452            I_CmsSerialDateBean bean = CmsSerialDateBeanFactory.createSerialDateBean(m_seriesDefinition);
453            m_dates = bean.getDates();
454            m_duration = bean.getEventDuration();
455        } else {
456            try {
457                throw new Exception("Could not read series definition: " + seriesDefinitionString);
458            } catch (Exception e) {
459                LOG.debug(e.getMessage(), e);
460            }
461            m_dates = new TreeSet<>();
462        }
463    }
464}