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.main.CmsLog;
032import org.opencms.util.CmsCollectionsGenericWrapper;
033
034import java.text.DateFormat;
035import java.text.SimpleDateFormat;
036import java.util.Calendar;
037import java.util.Date;
038import java.util.GregorianCalendar;
039import java.util.Locale;
040import java.util.Map;
041
042import org.apache.commons.collections.Transformer;
043import org.apache.commons.logging.Log;
044
045/**
046 * An instance of a date series with a start and optional end time,
047 * usable to describe one date for events and similar contents.<p>
048 *
049 * Provides convenient methods to format the date or date range.<p>
050 */
051public class CmsJspInstanceDateBean {
052
053    /** Formatting options for dates. */
054    public static class CmsDateFormatOption {
055
056        /** The date format. */
057        SimpleDateFormat m_dateFormat;
058        /** The time format. */
059        SimpleDateFormat m_timeFormat;
060        /** The date and time format. */
061        SimpleDateFormat m_dateTimeFormat;
062
063        /**
064         * Create a new date format option.
065         *
066         * Examples (for date 19/06/82 11:17):
067         * <ul>
068         *   <li>"dd/MM/yy"
069         *      <ul>
070         *          <li>formatDate: "19/06/82"</li>
071         *          <li>formatTime: ""</li>
072         *          <li>formatDateTime: "19/06/82"</li>
073         *      </ul>
074         *   </li>
075         *   <li>"dd/MM/yy|hh:mm"
076         *      <ul>
077         *          <li>formatDate: "19/06/82"</li>
078         *          <li>formatTime: "11:17"</li>
079         *          <li>formatDateTime: "19/06/82 11:17"</li>
080         *      </ul>
081         *   </li>
082         *   <li>"dd/MM/yy|hh:mm|dd/MM/yy - hh:mm"
083         *      <ul>
084         *          <li>formatDate: "19/06/82"</li>
085         *          <li>formatTime: "11:17"</li>
086         *          <li>formatDateTime: "19/06/82 - 11:17"</li>
087         *      </ul>
088         *   </li>
089         * @param configString the configuration string, should be structured as "datePattern|timePattern|dateTimePattern", where only datePattern is mandatory.
090         * @param locale the locale to use for printing days of week, month names etc.
091         * @throws IllegalArgumentException thrown if the configured patterns are invalid.
092         */
093        public CmsDateFormatOption(String configString, Locale locale)
094        throws IllegalArgumentException {
095
096            if (null != configString) {
097                String[] config = configString.split("\\|");
098                String datePattern = config[0];
099                if (!datePattern.trim().isEmpty()) {
100                    m_dateFormat = new SimpleDateFormat(datePattern, locale);
101                }
102                if (config.length > 1) {
103                    String timePattern = config[1];
104                    if (!timePattern.trim().isEmpty()) {
105                        m_timeFormat = new SimpleDateFormat(timePattern, locale);
106                    }
107                    if (config.length > 2) {
108                        String dateTimePattern = config[2];
109                        if (!dateTimePattern.trim().isEmpty()) {
110                            m_dateTimeFormat = new SimpleDateFormat(dateTimePattern, locale);
111                        }
112                    } else if ((null != m_dateFormat) && (null != m_timeFormat)) {
113                        m_dateTimeFormat = new SimpleDateFormat(
114                            m_dateFormat.toPattern() + " " + m_timeFormat.toPattern(),
115                            locale);
116                    }
117                }
118            }
119        }
120
121        /**
122         * Returns the formatted date (without time).
123         * @param d the {@link Date} to format.
124         * @return the formatted date (without time).
125         */
126        String formatDate(Date d) {
127
128            return null != m_dateFormat ? m_dateFormat.format(d) : "";
129        }
130
131        /**
132         * Returns the formatted date (with time).
133         * @param d the {@link Date} to format.
134         * @return the formatted date (with time).
135         */
136        String formatDateTime(Date d) {
137
138            return null != m_dateTimeFormat
139            ? m_dateTimeFormat.format(d)
140            : null != m_dateFormat ? m_dateFormat.format(d) : m_timeFormat != null ? m_timeFormat.format(d) : "";
141        }
142
143        /**
144         * Returns the formatted time (without date).
145         * @param d the {@link Date} to format.
146         * @return the formatted time (without date).
147         */
148        String formatTime(Date d) {
149
150            return null != m_timeFormat ? m_timeFormat.format(d) : "";
151        }
152    }
153
154    /** Transformer from formatting options to formatted dates. */
155    public class CmsDateFormatTransformer implements Transformer {
156
157        /** The locale to use for formatting (e.g. for the names of month). */
158        Locale m_locale;
159
160        /**
161         * Constructor for the date format transformer.
162         * @param locale the locale to use for writing names of month or days of weeks etc.
163         */
164        public CmsDateFormatTransformer(Locale locale) {
165
166            m_locale = locale;
167        }
168
169        /**
170         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
171         */
172        public String transform(Object formatOption) {
173
174            CmsDateFormatOption option = null;
175            try {
176                option = new CmsDateFormatOption(formatOption.toString(), m_locale);
177            } catch (IllegalArgumentException e) {
178                LOG.error(
179                    "At least one of the provided date/time patterns are illegal. Defaulting to short default date format.",
180                    e);
181            }
182            return getFormattedDate(option);
183        }
184
185    }
186
187    /** The log object for this class. */
188    static final Log LOG = CmsLog.getLog(CmsJspInstanceDateBean.class);
189
190    /** The separator between start and end date to use when formatting dates. */
191    private static final String DATE_SEPARATOR = " - ";
192
193    /** Beginning of this instance date. */
194    private Date m_start;
195
196    /** End of this instance date. */
197    private Date m_end;
198
199    /** Explicitely set end of the instance date. */
200    private Date m_explicitEnd;
201
202    /** Indicates if this instance date explicitely lasts the whole day. */
203    private Boolean m_explicitWholeDay;
204
205    /** Explicitely set locale of this instance date. */
206    private Locale m_explicitLocale;
207
208    /** The series this instance date is part of. */
209    private CmsJspDateSeriesBean m_series;
210
211    /** The dates of this instance date formatted locale specific in long style. */
212    private String m_formatLong;
213
214    /** The dates of this instance date formatted locale specific in short style. */
215    private String m_formatShort;
216
217    /** The formatted dates as lazy map. */
218    private Map<String, String> m_formattedDates;
219
220    /**
221     * Empty Constructor, for use as JavaBean.<p>
222     *
223     * Requires to call one of the init() methods later.<p>
224     *
225     * @see #init(Date, Locale)
226     */
227    public CmsJspInstanceDateBean() {
228
229        // noop
230    }
231
232    /**
233     * Constructor taking start and the date series this instance date belongs to.<p>
234     *
235     * @param start the start date for this instance date
236     * @param series the date series this instance date belongs to
237     */
238    public CmsJspInstanceDateBean(Date start, CmsJspDateSeriesBean series) {
239
240        m_start = start;
241        m_series = series;
242    }
243
244    /**
245     * Constructor to wrap a single date as instance date.<p>
246     *
247     * This will allow to use the format options.<p>
248     *
249     * @param start the start date for this instance date
250     * @param locale the locale used to format the date
251     *
252     */
253    public CmsJspInstanceDateBean(Date start, Locale locale) {
254
255        this(start, new CmsJspDateSeriesBean(Long.toString(start.getTime()), locale));
256    }
257
258    /**
259     * Returns the end time of this instance date.<p>
260     *
261     * @return the end time of this instance date
262     */
263    public Date getEnd() {
264
265        if (m_explicitEnd != null) {
266            return isWholeDay() ? adjustForWholeDay(m_explicitEnd, true) : m_explicitEnd;
267        }
268        if ((m_end == null) && (m_series.getInstanceDuration() != null)) {
269            m_end = new Date(m_start.getTime() + m_series.getInstanceDuration().longValue());
270        }
271        if (m_end == null) {
272            m_end = new Date(getStart().getTime());
273        }
274        return (m_end.getTime() > 0) && isWholeDay() && !m_series.isWholeDay() ? adjustForWholeDay(m_end, true) : m_end;
275    }
276
277    /**
278     * Returns an instance date bean wrapping only the end date of the original bean.
279     * @return an instance date bean wrapping only the end date of the original bean.
280     */
281    public CmsJspInstanceDateBean getEndInstance() {
282
283        return new CmsJspInstanceDateBean(getEnd(), m_series.getLocale());
284    }
285
286    /**
287     * Returns a lazy map from date format options to dates.
288     * Supported formats are the values of {@link CmsDateFormatOption}.<p>
289     *
290     * Each option must be backed up by four three keys in the message "bundle org.opencms.jsp.util.messages" for you locale:
291     * GUI_PATTERN_DATE_{Option}, GUI_PATTERN_DATE_TIME_{Option} and GUI_PATTERN_TIME_{Option}.
292     *
293     * @return a lazy map from date patterns to dates.
294     */
295    public Map<String, String> getFormat() {
296
297        if (null == m_formattedDates) {
298            m_formattedDates = CmsCollectionsGenericWrapper.createLazyMap(
299                new CmsDateFormatTransformer(m_series.getLocale()));
300        }
301        return m_formattedDates;
302
303    }
304
305    /**
306     * Returns the start and end dates/times as "start - end" in long date format and short time format specific for the request locale.
307     * @return the formatted date/time string.
308     */
309    public String getFormatLong() {
310
311        if (m_formatLong == null) {
312            m_formatLong = getFormattedDate(DateFormat.LONG);
313        }
314        return m_formatLong;
315    }
316
317    /**
318     * Returns the start and end dates/times as "start - end" in short date/time format specific for the request locale.
319     * @return the formatted date/time string.
320     */
321    public String getFormatShort() {
322
323        if (m_formatShort == null) {
324            m_formatShort = getFormattedDate(DateFormat.SHORT);
325        }
326        return m_formatShort;
327    }
328
329    /**
330     * Checks if this instance date has been set or initialized.<p>
331     *
332     * If the start date of the instance date is 0 milliseconds, we assume the instance date has not been set.<p>
333     *
334     * @return true if this instance date has been set or initialized
335     */
336    public boolean getIsSet() {
337
338        return (m_start != null) && (m_start.getTime() != 0);
339    }
340
341    /**
342     * Returns a time of the last day where this instance date takes place.<p>
343     *
344     * This can be used to output the last calendar day of this instance date without time.<p>
345     *
346     * @return a time of the last day where this instance date takes place
347     */
348    public Date getLastDay() {
349
350        // for whole day instance dates the end date is adjusted by subtracting one day,
351        // otherwise the period would be one day too long
352        return isWholeDay() ? new Date(getEnd().getTime() - I_CmsSerialDateValue.DAY_IN_MILLIS) : getEnd();
353    }
354
355    /**
356     * Returns the start time of this instance date.<p>
357     *
358     * @return the start time of this instance date
359     */
360    public Date getStart() {
361
362        // Adjust the start time for an explicitely whole day option that overwrites the series' whole day option.
363        if (m_start == null) {
364            m_start = new Date(0);
365        }
366        return (m_start.getTime() > 0) && isWholeDay() && !m_series.isWholeDay()
367        ? adjustForWholeDay(m_start, false)
368        : m_start;
369    }
370
371    /**
372     * Returns an instance date bean wrapping only the start date of the original bean.<p>
373     *
374     * @return an instance date bean wrapping only the start date of the original bean
375     */
376    public CmsJspInstanceDateBean getStartInstance() {
377
378        return new CmsJspInstanceDateBean(getStart(), m_series.getLocale());
379    }
380
381    /**
382     * Initializes this date instance.<p>
383     *
384     * Use this only in case this date instance has been created as a JavaBean.<p>
385     *
386     * @param start the start date for this instance date
387     * @param locale the locale used to format the date
388     */
389    public void init(Date start, Locale locale) {
390
391        m_start = start;
392        m_series = new CmsJspDateSeriesBean(Long.toString(start.getTime()), locale);
393    }
394
395    /**
396     * Initializes this date instance with a String for the locale.<p>
397     *
398     * Use this only in case this date instance has been created as a JavaBean.<p>
399     *
400     * @param start the start date for this instance date
401     * @param localeStr a String representing the locale used to format the date
402     */
403    public void init(Date start, String localeStr) {
404
405        init(start, new Locale(localeStr));
406    }
407
408    /**
409     * Returns a flag, indicating if this instance date last over night.<p>
410     *
411     * @return <code>true</code> if this instance date ends on another day than it starts, <code>false</code> if it ends on the same day.
412     */
413    public boolean isMultiDay() {
414
415        if ((null != m_explicitEnd) || (null != m_explicitWholeDay)) {
416            return isSingleMultiDay();
417        } else {
418            return m_series.isMultiDay();
419        }
420    }
421
422    /**
423     * Indicates if this instance date lasts whole days.<p>
424     *
425     * @return true if this instance date lasts whole days
426     */
427    public boolean isWholeDay() {
428
429        return null == m_explicitWholeDay ? m_series.isWholeDay() : m_explicitWholeDay.booleanValue();
430    }
431
432    /**
433     * Explicitly set the end time of this instance date.<p>
434     *
435     * If the provided date is <code>null</code> or a date before the start date, the end date defaults to the start date.
436     *
437     * @param endDate the end time of this instance date
438     */
439    public void setEnd(Date endDate) {
440
441        if ((null == endDate) || getStart().after(endDate)) {
442            m_explicitEnd = null;
443        } else {
444            m_explicitEnd = endDate;
445        }
446    }
447
448    /**
449     * Explicitly set the end time of this instance date using a long value.<p>
450     *
451     * If the provided date is <code>null</code> or a date before the start date, the end date defaults to the start date.
452     *
453     * @param endDate  the end time of this instance date
454     */
455    public void setEnd(long endDate) {
456
457        m_formatLong = null;
458        m_formatShort = null;
459        setEnd(new Date(endDate));
460    }
461
462    /**
463     * Set if this instance date is whole day.
464     *
465     * @param isWholeDay flag, indicating if this instance date lasts the whole day -
466     *          if <code>null</code> the value defaults to the setting from the underlying date series.
467     */
468    public void setWholeDay(Boolean isWholeDay) {
469
470        m_formatLong = null;
471        m_formatShort = null;
472        m_explicitWholeDay = isWholeDay;
473    }
474
475    /**
476     * Returns the start and end dates/times as "start - end" in the provided date/time format specific for the request locale.
477     * @param formatOption the format to use for date and time.
478     * @return the formatted date/time string.
479     */
480    String getFormattedDate(CmsDateFormatOption formatOption) {
481
482        if (null == formatOption) {
483            return getFormattedDate(DateFormat.SHORT);
484        }
485        String result;
486        if (isWholeDay()) {
487            result = formatOption.formatDate(getStart());
488            if (getLastDay().after(getStart())) {
489                String to = formatOption.formatDate(getLastDay());
490                if (!to.isEmpty()) {
491                    result += DATE_SEPARATOR + to;
492                }
493            }
494        } else {
495            result = formatOption.formatDateTime(getStart());
496            if (getEnd().after(getStart())) {
497                String to;
498                if (isMultiDay()) {
499                    to = formatOption.formatDateTime(getEnd());
500                } else {
501                    to = formatOption.formatTime(getEnd());
502                }
503                if (!to.isEmpty()) {
504                    result += DATE_SEPARATOR + to;
505                }
506            }
507        }
508
509        return result;
510    }
511
512    /**
513     * Adjust the date according to the whole day options.<p>
514     *
515     * @param date the date to adjust
516     * @param isEnd true if the date is the end of this instance date (in contrast to the beginning)
517     *
518     * @return the adjusted date, which will be exactly the beginning or the end of the provide date's day
519     */
520    private Date adjustForWholeDay(Date date, boolean isEnd) {
521
522        Calendar result = new GregorianCalendar();
523        result.setTime(date);
524        result.set(Calendar.HOUR_OF_DAY, 0);
525        result.set(Calendar.MINUTE, 0);
526        result.set(Calendar.SECOND, 0);
527        result.set(Calendar.MILLISECOND, 0);
528        if (isEnd) {
529            result.add(Calendar.DATE, 1);
530        }
531
532        return result.getTime();
533    }
534
535    /**
536     * Returns the start and end dates/times as "start - end" in the provided date/time format specific for the request locale.<p>
537     *
538     * @param dateTimeFormat the format to use for date (time is always short)
539     * @return the formatted date/time string
540     */
541    private String getFormattedDate(int dateTimeFormat) {
542
543        DateFormat df;
544        String result;
545        if (isWholeDay()) {
546            df = DateFormat.getDateInstance(dateTimeFormat, m_series.getLocale());
547            result = df.format(getStart());
548            if (getLastDay().after(getStart())) {
549                result += DATE_SEPARATOR + df.format(getLastDay());
550            }
551        } else {
552            df = DateFormat.getDateTimeInstance(dateTimeFormat, DateFormat.SHORT, m_series.getLocale());
553            result = df.format(getStart());
554            if (getEnd().after(getStart())) {
555                if (isMultiDay()) {
556                    result += DATE_SEPARATOR + df.format(getEnd());
557                } else {
558                    df = DateFormat.getTimeInstance(DateFormat.SHORT, m_series.getLocale());
559                    result += DATE_SEPARATOR + df.format(getEnd());
560                }
561            }
562        }
563
564        return result;
565    }
566
567    /**
568     * Returns a flag, indicating if this instance date is multi-day.<p>
569     *
570     * The method is only called if this instance date has an explicitely set end date
571     * or an explicitely changed whole day option.<p>
572     *
573     * @return true if this instance date is multi-day
574     */
575    private boolean isSingleMultiDay() {
576
577        long duration = getEnd().getTime() - getStart().getTime();
578        if (duration > I_CmsSerialDateValue.DAY_IN_MILLIS) {
579            return true;
580        }
581        if (isWholeDay() && (duration <= I_CmsSerialDateValue.DAY_IN_MILLIS)) {
582            return false;
583        }
584        Calendar start = new GregorianCalendar();
585        start.setTime(getStart());
586        Calendar end = new GregorianCalendar();
587        end.setTime(getEnd());
588        if (start.get(Calendar.DAY_OF_MONTH) == end.get(Calendar.DAY_OF_MONTH)) {
589            return false;
590        }
591        return true;
592
593    }
594}