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