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.widgets.serialdate;
029
030import org.opencms.acacia.shared.A_CmsSerialDateValue;
031import org.opencms.acacia.shared.CmsSerialDateUtil;
032import org.opencms.i18n.CmsMessageContainer;
033import org.opencms.json.JSONArray;
034import org.opencms.json.JSONException;
035import org.opencms.json.JSONObject;
036import org.opencms.jsp.util.CmsJspElFunctions;
037import org.opencms.main.CmsLog;
038import org.opencms.util.CmsUUID;
039
040import java.util.Collection;
041import java.util.Date;
042import java.util.SortedSet;
043import java.util.TreeSet;
044
045import org.apache.commons.logging.Log;
046
047/** Server-side implementation of {@link org.opencms.acacia.shared.I_CmsSerialDateValue}. */
048public class CmsSerialDateValue extends A_CmsSerialDateValue {
049
050    /** Logger for the class. */
051    private static final Log LOG = CmsLog.getLog(CmsSerialDateValue.class);
052
053    /** Flag, indicating if parsing the provided string value failed. */
054    private boolean m_parsingFailed;
055
056    /** Default constructor, setting the default state of the the serial date widget. */
057    public CmsSerialDateValue() {
058
059        setDefaultValue();
060    }
061
062    /**
063     * Wraps the JSON specification of the serial date.
064     *
065     * @param value JSON representation of the serial date as string.
066     */
067    public CmsSerialDateValue(String value) {
068        if ((null != value) && !value.isEmpty()) {
069            try {
070                JSONObject json = new JSONObject(value);
071                setStart(readOptionalDate(json, JsonKey.START));
072                setEnd(readOptionalDate(json, JsonKey.END));
073                setWholeDay(readOptionalBoolean(json, JsonKey.WHOLE_DAY));
074                JSONObject patternJson = json.getJSONObject(JsonKey.PATTERN);
075                readPattern(patternJson);
076                setExceptions(readDates(readOptionalArray(json, JsonKey.EXCEPTIONS)));
077                setSeriesEndDate(readOptionalDate(json, JsonKey.SERIES_ENDDATE));
078                setOccurrences(readOptionalInt(json, JsonKey.SERIES_OCCURRENCES));
079                setDerivedEndType();
080                setCurrentTillEnd(readOptionalBoolean(json, JsonKey.CURRENT_TILL_END));
081                setParentSeriesId(readOptionalUUID(json, JsonKey.PARENT_SERIES));
082            } catch (JSONException e) {
083                setDefaultValue();
084                Date d = CmsJspElFunctions.convertDate(value);
085                if (d.getTime() == 0) {
086                    m_parsingFailed = true;
087                } else {
088                    setStart(d);
089                }
090            }
091        } else {
092            setDefaultValue();
093        }
094    }
095
096    /**
097     * Convert the information from the wrapper to a JSON object.
098     * @return the serial date information as JSON.
099     */
100    public JSONObject toJson() {
101
102        try {
103            JSONObject result = new JSONObject();
104            if (null != getStart()) {
105                result.put(JsonKey.START, dateToJson(getStart()));
106            }
107            if (null != getEnd()) {
108                result.put(JsonKey.END, dateToJson(getEnd()));
109            }
110            if (isWholeDay()) {
111                result.put(JsonKey.WHOLE_DAY, true);
112            }
113            JSONObject pattern = patternToJson();
114            result.put(JsonKey.PATTERN, pattern);
115            SortedSet<Date> exceptions = getExceptions();
116            if (!exceptions.isEmpty()) {
117                result.put(JsonKey.EXCEPTIONS, datesToJson(exceptions));
118            }
119            switch (getEndType()) {
120                case DATE:
121                    result.put(JsonKey.SERIES_ENDDATE, dateToJson(getSeriesEndDate()));
122                    break;
123                case TIMES:
124                    result.put(JsonKey.SERIES_OCCURRENCES, String.valueOf(getOccurrences()));
125                    break;
126                case SINGLE:
127                default:
128                    break;
129            }
130            if (!isCurrentTillEnd()) {
131                result.put(JsonKey.CURRENT_TILL_END, false);
132            }
133            if (null != getParentSeriesId()) {
134                result.put(JsonKey.PARENT_SERIES, getParentSeriesId().getStringValue());
135            }
136            return result;
137        } catch (JSONException e) {
138            LOG.error("Could not convert Serial date value to JSON.", e);
139            return null;
140        }
141    }
142
143    /**
144     * @see java.lang.Object#toString()
145     */
146    @Override
147    public String toString() {
148
149        JSONObject json;
150        json = toJson();
151        return null != json ? json.toString() : "";
152    }
153
154    /**
155     * Validates the wrapped value and returns a localized error message in case of invalid values.
156     * @return <code>null</code> if the value is valid, a suitable localized error message otherwise.
157     */
158    public CmsMessageContainer validateWithMessage() {
159
160        if (m_parsingFailed) {
161            return Messages.get().container(Messages.ERR_SERIALDATE_INVALID_VALUE_0);
162        }
163        if (!isStartSet()) {
164            return Messages.get().container(Messages.ERR_SERIALDATE_START_MISSING_0);
165        }
166        if (!isEndValid()) {
167            return Messages.get().container(Messages.ERR_SERIALDATE_END_BEFORE_START_0);
168        }
169        String key = validatePattern();
170        if (null != key) {
171            return Messages.get().container(key);
172        }
173        key = validateDuration();
174        if (null != key) {
175            return Messages.get().container(key);
176        }
177        if (hasTooManyEvents()) {
178            return Messages.get().container(
179                Messages.ERR_SERIALDATE_TOO_MANY_EVENTS_1,
180                Integer.valueOf(CmsSerialDateUtil.getMaxEvents()));
181        }
182        return null;
183    }
184
185    /**
186     * Converts a list of dates to a Json array with the long representation of the dates as strings.
187     * @param individualDates the list to convert.
188     * @return Json array with long values of dates as string
189     */
190    private JSONArray datesToJson(Collection<Date> individualDates) {
191
192        if (null != individualDates) {
193            JSONArray result = new JSONArray();
194            for (Date d : individualDates) {
195                result.put(dateToJson(d));
196            }
197            return result;
198        }
199        return null;
200    }
201
202    /**
203     * Convert a date to the String representation we use in the JSON.
204     * @param d the date to convert
205     * @return the String representation we use in the JSON.
206     */
207    private String dateToJson(Date d) {
208
209        return Long.toString(d.getTime());
210    }
211
212    /**
213     * Returns a flag, indicating if the series has too many events.
214     * @return a flag, indicating if the series has too many events.
215     */
216    private boolean hasTooManyEvents() {
217
218        return CmsSerialDateBeanFactory.createSerialDateBean(this).hasTooManyDates();
219
220    }
221
222    /**
223     * Generates the JSON object storing the pattern information.
224     * @return the JSON object storing the pattern information.
225     * @throws JSONException if JSON creation fails.
226     */
227    private JSONObject patternToJson() throws JSONException {
228
229        JSONObject pattern = new JSONObject();
230        if (null != getPatternType()) {
231            pattern.putOpt(JsonKey.PATTERN_TYPE, getPatternType().toString());
232            switch (getPatternType()) {
233                case DAILY:
234                    if (isEveryWorkingDay()) {
235                        pattern.put(JsonKey.PATTERN_EVERYWORKINGDAY, true);
236                    } else {
237                        pattern.putOpt(JsonKey.PATTERN_INTERVAL, String.valueOf(getInterval()));
238                    }
239                    break;
240                case WEEKLY:
241                    pattern.putOpt(JsonKey.PATTERN_INTERVAL, String.valueOf(getInterval()));
242                    pattern.putOpt(JsonKey.PATTERN_WEEKDAYS, toJsonStringArray(getWeekDays()));
243                    break;
244                case MONTHLY:
245                    pattern.putOpt(JsonKey.PATTERN_INTERVAL, String.valueOf(getInterval()));
246                    if (null != getWeekDay()) {
247                        pattern.putOpt(JsonKey.PATTERN_WEEKS_OF_MONTH, toJsonStringArray(getWeeksOfMonth()));
248                        pattern.putOpt(JsonKey.PATTERN_WEEKDAYS, toJsonStringArray(getWeekDays()));
249                    } else {
250                        pattern.putOpt(JsonKey.PATTERN_DAY_OF_MONTH, "" + getDayOfMonth());
251                    }
252                    break;
253                case YEARLY:
254                    pattern.put(JsonKey.PATTERN_MONTH, getMonth().toString());
255                    if (null != getWeekDay()) {
256                        pattern.putOpt(JsonKey.PATTERN_WEEKS_OF_MONTH, toJsonStringArray(getWeeksOfMonth()));
257                        pattern.putOpt(JsonKey.PATTERN_WEEKDAYS, toJsonStringArray(getWeekDays()));
258                    } else {
259                        pattern.putOpt(JsonKey.PATTERN_DAY_OF_MONTH, "" + getDayOfMonth());
260                    }
261                    break;
262                case INDIVIDUAL:
263                    pattern.putOpt(JsonKey.PATTERN_DATES, datesToJson(getIndividualDates()));
264                    break;
265                case NONE:
266                default:
267                    break;
268            }
269        }
270        return pattern;
271    }
272
273    /**
274     * Extracts the dates from a JSON array.
275     * @param array the JSON array where the dates are stored in.
276     * @return list of the extracted dates.
277     */
278    private SortedSet<Date> readDates(JSONArray array) {
279
280        if (null != array) {
281            SortedSet<Date> result = new TreeSet<>();
282            for (int i = 0; i < array.length(); i++) {
283                try {
284                    long l = Long.valueOf(array.getString(i)).longValue();
285                    result.add(new Date(l));
286                } catch (NumberFormatException | JSONException e) {
287                    LOG.error("Could not read date from JSON array.", e);
288                }
289            }
290            return result;
291        }
292        return new TreeSet<>();
293    }
294
295    /**
296     * Read an optional JSON array.
297     * @param json the JSON Object that has the array as element
298     * @param key the key for the array in the provided JSON object
299     * @return the array or null if reading the array fails.
300     */
301    private JSONArray readOptionalArray(JSONObject json, String key) {
302
303        try {
304            return json.getJSONArray(key);
305        } catch (JSONException e) {
306            LOG.debug("Reading optional JSON array failed. Default to provided default value.", e);
307        }
308        return null;
309    }
310
311    /**
312     * Read an optional boolean value form a JSON Object.
313     * @param json the JSON object to read from.
314     * @param key the key for the boolean value in the provided JSON object.
315     * @return the boolean or null if reading the boolean fails.
316     */
317    private Boolean readOptionalBoolean(JSONObject json, String key) {
318
319        try {
320            return Boolean.valueOf(json.getBoolean(key));
321        } catch (JSONException e) {
322            LOG.debug("Reading optional JSON boolean failed. Default to provided default value.", e);
323        }
324        return null;
325    }
326
327    /**
328     * Read an optional Date value (stored as string) form a JSON Object.
329     * @param json the JSON object to read from.
330     * @param key the key for the Long value in the provided JSON object.
331     * @return the Date or null if reading the Date fails.
332     */
333    private Date readOptionalDate(JSONObject json, String key) {
334
335        try {
336            String str = json.getString(key);
337            return new Date(Long.parseLong(str));
338        } catch (NumberFormatException | JSONException e) {
339            LOG.debug("Reading optional JSON Long failed. Default to provided default value.", e);
340        }
341        return null;
342    }
343
344    /**
345     * Read an optional int value (stored as string) form a JSON Object.
346     * @param json the JSON object to read from.
347     * @param key the key for the int value in the provided JSON object.
348     * @return the int or 0 if reading the int fails.
349     */
350    private int readOptionalInt(JSONObject json, String key) {
351
352        try {
353            String str = json.getString(key);
354            return Integer.valueOf(str).intValue();
355        } catch (NumberFormatException | JSONException e) {
356            LOG.debug("Reading optional JSON int failed. Default to provided default value.", e);
357        }
358        return 0;
359    }
360
361    /**
362     * Read an optional month value (stored as string) form a JSON Object.
363     * @param json the JSON object to read from.
364     * @param key the key for the month value in the provided JSON object.
365     * @return the month or null if reading the month fails.
366     */
367    private Month readOptionalMonth(JSONObject json, String key) {
368
369        try {
370            String str = json.getString(key);
371            return Month.valueOf(str);
372        } catch (JSONException | IllegalArgumentException e) {
373            LOG.debug("Reading optional JSON month failed. Default to provided default value.", e);
374        }
375        return null;
376    }
377
378    /**
379     * Read an optional string value form a JSON Object.
380     * @param json the JSON object to read from.
381     * @param key the key for the string value in the provided JSON object.
382     * @param defaultValue the default value, to be returned if the string can not be read from the JSON object.
383     * @return the string or the default value if reading the string fails.
384     */
385    private String readOptionalString(JSONObject json, String key, String defaultValue) {
386
387        try {
388            String str = json.getString(key);
389            if (str != null) {
390                return str;
391            }
392
393        } catch (JSONException e) {
394            LOG.debug("Reading optional JSON string failed. Default to provided default value.", e);
395        }
396        return defaultValue;
397    }
398
399    /**
400     * Read an optional uuid stored as JSON string.
401     * @param json the JSON object to readfrom.
402     * @param key the key for the UUID in the provided JSON object.
403     * @return the uuid, or <code>null</code> if the uuid can not be read.
404     */
405    private CmsUUID readOptionalUUID(JSONObject json, String key) {
406
407        String id = readOptionalString(json, key, null);
408        if (null != id) {
409            try {
410                CmsUUID uuid = CmsUUID.valueOf(id);
411                return uuid;
412            } catch (NumberFormatException e) {
413                LOG.debug("Reading optional UUID failed. Could not convert \"" + id + "\" to a valid UUID.");
414            }
415        }
416        return null;
417    }
418
419    /**
420     * Read pattern information from the provided JSON object.
421     * @param patternJson the JSON object containing the pattern information.
422     */
423    private void readPattern(JSONObject patternJson) {
424
425        setPatternType(readPatternType(patternJson));
426        setInterval(readOptionalInt(patternJson, JsonKey.PATTERN_INTERVAL));
427        setWeekDays(readWeekDays(patternJson));
428        setDayOfMonth(readOptionalInt(patternJson, JsonKey.PATTERN_DAY_OF_MONTH));
429        setEveryWorkingDay(readOptionalBoolean(patternJson, JsonKey.PATTERN_EVERYWORKINGDAY));
430        setWeeksOfMonth(readWeeksOfMonth(patternJson));
431        setIndividualDates(readDates(readOptionalArray(patternJson, JsonKey.PATTERN_DATES)));
432        setMonth(readOptionalMonth(patternJson, JsonKey.PATTERN_MONTH));
433
434    }
435
436    /**
437     * Reads the pattern type from the provided JSON. Defaults to type NONE.
438     * @param val the JSON object containing the pattern type information.
439     * @return the pattern type.
440     */
441    private PatternType readPatternType(JSONObject val) {
442
443        PatternType patterntype;
444        try {
445            String str = readOptionalString(val, JsonKey.PATTERN_TYPE, "");
446            patterntype = PatternType.valueOf(str);
447        } catch (IllegalArgumentException e) {
448            LOG.debug("Could not read pattern type from JSON. Default to type NONE.", e);
449            patterntype = PatternType.NONE;
450        }
451        return patterntype;
452    }
453
454    /**
455     * Reads the weekday information.
456     * @param val the JSON object containing the weekday information.
457     * @return the weekdays extracted, defaults to the empty list, if no information is found.
458     */
459    private SortedSet<WeekDay> readWeekDays(JSONObject val) {
460
461        try {
462            JSONArray array = val.getJSONArray(JsonKey.PATTERN_WEEKDAYS);
463            if (null != array) {
464                SortedSet<WeekDay> result = new TreeSet<>();
465                for (int i = 0; i < array.length(); i++) {
466                    try {
467                        result.add(WeekDay.valueOf(array.getString(i)));
468                    } catch (JSONException | IllegalArgumentException e) {
469                        LOG.error("Could not read weekday from JSON. Skipping that day.", e);
470                    }
471                }
472                return result;
473            }
474        } catch (JSONException e) {
475            LOG.debug("Could not read weekdays from JSON", e);
476        }
477        return new TreeSet<>();
478
479    }
480
481    /**
482     * Read "weeks of month" information from JSON.
483     * @param json the JSON where information is read from.
484     * @return the list of weeks read, defaults to the empty list.
485     */
486    private SortedSet<WeekOfMonth> readWeeksOfMonth(JSONObject json) {
487
488        try {
489            JSONArray array = json.getJSONArray(JsonKey.PATTERN_WEEKS_OF_MONTH);
490            if (null != array) {
491                SortedSet<WeekOfMonth> result = new TreeSet<>();
492                for (int i = 0; i < array.length(); i++) {
493                    try {
494                        WeekOfMonth week = WeekOfMonth.valueOf(array.getString(i));
495                        result.add(week);
496                    } catch (JSONException e) {
497                        LOG.debug("Could not read week of month from JSON. Skipping that particular week of month.", e);
498                    }
499                }
500                return result;
501            }
502        } catch (JSONException e) {
503            LOG.debug("Could not read week of month information from JSON", e);
504        }
505        return new TreeSet<>();
506    }
507
508    /**
509     * Convert a collection of objects to a JSON array with the string representations of that objects.
510     * @param collection the collection of objects.
511     * @return the JSON array with the string representations.
512     */
513    private JSONArray toJsonStringArray(Collection<? extends Object> collection) {
514
515        if (null != collection) {
516            JSONArray array = new JSONArray();
517            for (Object o : collection) {
518                array.put("" + o);
519            }
520            return array;
521        } else {
522            return null;
523        }
524    }
525
526    /**
527     * Check if the day of month is valid.
528     * @return <code>null</code> if the day of month is valid, the key of a suitable error message otherwise.
529     */
530    private String validateDayOfMonth() {
531
532        return (isDayOfMonthValid()) ? null : Messages.ERR_SERIALDATE_INVALID_DAY_OF_MONTH_0;
533    }
534
535    /**
536     * Checks if the provided duration information is valid.
537     * @return <code>null</code> if the information is valid, the key of the suitable error message otherwise.
538     */
539    private String validateDuration() {
540
541        if (!isValidEndTypeForPattern()) {
542            return Messages.ERR_SERIALDATE_INVALID_END_TYPE_FOR_PATTERN_0;
543        }
544        switch (getEndType()) {
545            case DATE:
546                return (getStart().getTime() < (getSeriesEndDate().getTime() + DAY_IN_MILLIS))
547                ? null
548                : Messages.ERR_SERIALDATE_SERIES_END_BEFORE_START_0;
549            case TIMES:
550                return getOccurrences() > 0 ? null : Messages.ERR_SERIALDATE_INVALID_OCCURRENCES_0;
551            default:
552                return null;
553        }
554
555    }
556
557    /**
558     * Check if the interval is valid.
559     * @return <code>null</code> if the interval is valid, a suitable error message key otherwise.
560     */
561    private String validateInterval() {
562
563        return isIntervalValid() ? null : Messages.ERR_SERIALDATE_INVALID_INTERVAL_0;
564    }
565
566    /**
567     * Check, if the month is set.
568     * @return <code>null</code> if a month is set, a suitable error message key otherwise.
569     */
570    private String validateMonthSet() {
571
572        return isMonthSet() ? null : Messages.ERR_SERIALDATE_NO_MONTH_SET_0;
573    }
574
575    /**
576     * Check, if all values used for calculating the series for a specific pattern are valid.
577     * @return <code>null</code> if the pattern is valid, a suitable error message otherwise.
578     */
579    private String validatePattern() {
580
581        String error = null;
582        switch (getPatternType()) {
583            case DAILY:
584                error = isEveryWorkingDay() ? null : validateInterval();
585                break;
586            case WEEKLY:
587                error = validateInterval();
588                if (null == error) {
589                    error = validateWeekDaySet();
590                }
591                break;
592            case MONTHLY:
593                error = validateInterval();
594                if (null == error) {
595                    error = validateMonthSet();
596                    if (null == error) {
597                        error = isWeekDaySet() ? validateWeekOfMonthSet() : validateDayOfMonth();
598                    }
599                }
600                break;
601            case YEARLY:
602                error = isWeekDaySet() ? validateWeekOfMonthSet() : validateDayOfMonth();
603                break;
604            case INDIVIDUAL:
605            case NONE:
606            default:
607        }
608        return error;
609    }
610
611    /**
612     * Validate if a weekday is set, otherwise return the key for a suitable error message.
613     * @return <code>null</code> if a weekday is set, the key for a suitable error message otherwise.
614     */
615    private String validateWeekDaySet() {
616
617        return isWeekDaySet() ? null : Messages.ERR_SERIALDATE_NO_WEEKDAY_SPECIFIED_0;
618    }
619
620    /**
621     * Check if a week of month is set.
622     * @return <code>null</code> if a week of month is set, the key for a suitable error message otherwise.
623     */
624    private String validateWeekOfMonthSet() {
625
626        return isWeekOfMonthSet() ? null : Messages.ERR_SERIALDATE_NO_WEEK_OF_MONTH_SPECIFIED_0;
627    }
628
629}