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