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.acacia.client.widgets.serialdate;
029
030import org.opencms.acacia.shared.A_CmsSerialDateValue;
031import org.opencms.acacia.shared.I_CmsSerialDateValue;
032import org.opencms.gwt.client.util.CmsDebugLog;
033import org.opencms.util.CmsUUID;
034
035import java.util.Collection;
036import java.util.Date;
037import java.util.HashSet;
038import java.util.SortedSet;
039import java.util.TreeSet;
040
041import com.google.gwt.json.client.JSONArray;
042import com.google.gwt.json.client.JSONBoolean;
043import com.google.gwt.json.client.JSONObject;
044import com.google.gwt.json.client.JSONParser;
045import com.google.gwt.json.client.JSONString;
046import com.google.gwt.json.client.JSONValue;
047
048/**
049 * Client-side implementation of {@link I_CmsSerialDateValue}.
050 * The implementation additionally has setters for the various values of the serial date specification.
051 */
052public class CmsSerialDateValue extends A_CmsSerialDateValue implements I_CmsObservableSerialDateValue {
053
054    /** The list of value change observers. */
055    Collection<I_CmsSerialDateValueChangeObserver> m_valueChangeObservers = new HashSet<>();
056
057    /** Default constructor, setting the default state of the the serial date widget. */
058    public CmsSerialDateValue() {
059
060        setDefaultValue();
061    }
062
063    /**
064     * @see org.opencms.acacia.client.widgets.serialdate.I_CmsObservableSerialDateValue#registerValueChangeObserver(org.opencms.acacia.client.widgets.serialdate.I_CmsSerialDateValueChangeObserver)
065     */
066    public void registerValueChangeObserver(I_CmsSerialDateValueChangeObserver obs) {
067
068        m_valueChangeObservers.add(obs);
069
070    }
071
072    /**
073     * Set the value as provided.
074     * @param value the serial date value as JSON string.
075     */
076    public final void setValue(String value) {
077
078        if ((null == value) || value.isEmpty()) {
079            setDefaultValue();
080        } else {
081            try {
082                tryToSetParsedValue(value);
083            } catch (@SuppressWarnings("unused") Exception e) {
084                CmsDebugLog.consoleLog("Could not set invalid serial date value: " + value);
085                setDefaultValue();
086            }
087        }
088        notifyOnValueChange();
089    }
090
091    /**
092     * Convert the information from the wrapper to a JSON object.
093     * @return the serial date information as JSON.
094     */
095    public JSONValue toJson() {
096
097        JSONObject result = new JSONObject();
098        if (null != getStart()) {
099            result.put(JsonKey.START, dateToJson(getStart()));
100        }
101        if (null != getEnd()) {
102            result.put(JsonKey.END, dateToJson(getEnd()));
103        }
104        if (isWholeDay()) {
105            result.put(JsonKey.WHOLE_DAY, JSONBoolean.getInstance(true));
106        }
107        JSONObject pattern = patternToJson();
108        result.put(JsonKey.PATTERN, pattern);
109        SortedSet<Date> exceptions = getExceptions();
110        if (exceptions.size() > 0) {
111            result.put(JsonKey.EXCEPTIONS, datesToJsonArray(exceptions));
112        }
113        switch (getEndType()) {
114            case DATE:
115                result.put(JsonKey.SERIES_ENDDATE, dateToJson(getSeriesEndDate()));
116                break;
117            case TIMES:
118                result.put(JsonKey.SERIES_OCCURRENCES, new JSONString(String.valueOf(getOccurrences())));
119                break;
120            case SINGLE:
121            default:
122                break;
123        }
124        if (!isCurrentTillEnd()) {
125            result.put(JsonKey.CURRENT_TILL_END, JSONBoolean.getInstance(false));
126        }
127        if (getParentSeriesId() != null) {
128            result.put(JsonKey.PARENT_SERIES, new JSONString(getParentSeriesId().getStringValue()));
129        }
130        return result;
131    }
132
133    /**
134     * @see java.lang.Object#toString()
135     */
136    @Override
137    public String toString() {
138
139        JSONValue json = toJson();
140        return json.toString();
141    }
142
143    /**
144     * @see org.opencms.acacia.client.widgets.serialdate.I_CmsObservableSerialDateValue#unregisterValueChangeObserver(org.opencms.acacia.client.widgets.serialdate.I_CmsSerialDateValueChangeObserver)
145     */
146    public void unregisterValueChangeObserver(I_CmsSerialDateValueChangeObserver obs) {
147
148        m_valueChangeObservers.remove(obs);
149
150    }
151
152    /**
153     * Converts a collection of dates to a JSON array with the long representation of the dates as strings.
154     * @param dates the list to convert.
155     * @return JSON array with long values of dates as string
156     */
157    private JSONValue datesToJsonArray(Collection<Date> dates) {
158
159        if (null != dates) {
160            JSONArray result = new JSONArray();
161            for (Date d : dates) {
162                result.set(result.size(), dateToJson(d));
163            }
164            return result;
165        }
166        return null;
167    }
168
169    /**
170     * Convert a date to the String representation we use in the JSON.
171     * @param d the date to convert
172     * @return the String representation we use in the JSON.
173     */
174    private JSONValue dateToJson(Date d) {
175
176        return null != d ? new JSONString(Long.toString(d.getTime())) : null;
177    }
178
179    /**
180     * Notifies all observers on value changes.
181     */
182    private void notifyOnValueChange() {
183
184        for (I_CmsSerialDateValueChangeObserver obs : m_valueChangeObservers) {
185            obs.onValueChange();
186        }
187
188    }
189
190    /**
191     * Generates the JSON object storing the pattern information.
192     * @return the JSON object storing the pattern information.
193     */
194    private JSONObject patternToJson() {
195
196        JSONObject pattern = new JSONObject();
197        if (null != getPatternType()) {
198            pattern.put(JsonKey.PATTERN_TYPE, new JSONString(getPatternType().toString()));
199            switch (getPatternType()) {
200                case DAILY:
201                    if (isEveryWorkingDay()) {
202                        pattern.put(JsonKey.PATTERN_EVERYWORKINGDAY, JSONBoolean.getInstance(true));
203                    } else {
204                        pattern.put(JsonKey.PATTERN_INTERVAL, new JSONString(String.valueOf(getInterval())));
205                    }
206                    break;
207                case WEEKLY:
208                    pattern.put(JsonKey.PATTERN_INTERVAL, new JSONString(String.valueOf(getInterval())));
209                    pattern.put(JsonKey.PATTERN_WEEKDAYS, toJsonStringList(getWeekDays()));
210                    break;
211                case MONTHLY:
212                    pattern.put(JsonKey.PATTERN_INTERVAL, new JSONString(String.valueOf(getInterval())));
213                    if (null != getWeekDay()) {
214                        pattern.put(JsonKey.PATTERN_WEEKS_OF_MONTH, toJsonStringList(getWeeksOfMonth()));
215                        pattern.put(JsonKey.PATTERN_WEEKDAYS, toJsonStringList(getWeekDays()));
216                    } else {
217                        pattern.put(JsonKey.PATTERN_DAY_OF_MONTH, new JSONString(String.valueOf(getDayOfMonth())));
218                    }
219                    break;
220                case YEARLY:
221                    pattern.put(JsonKey.PATTERN_MONTH, new JSONString(String.valueOf(getMonth())));
222                    if (null != getWeekDay()) {
223                        pattern.put(JsonKey.PATTERN_WEEKS_OF_MONTH, toJsonStringList(getWeeksOfMonth()));
224                        pattern.put(JsonKey.PATTERN_WEEKDAYS, toJsonStringList(getWeekDays()));
225                    } else {
226                        pattern.put(JsonKey.PATTERN_DAY_OF_MONTH, new JSONString(String.valueOf(getDayOfMonth())));
227                    }
228                    break;
229                case INDIVIDUAL:
230                    pattern.put(JsonKey.PATTERN_DATES, datesToJsonArray(getIndividualDates()));
231                    break;
232                case NONE:
233                default:
234                    break;
235            }
236        }
237        return pattern;
238    }
239
240    /**
241     * Extracts the dates from a JSON array.
242     * @param json the JSON array where the dates are stored in.
243     * @return list of the extracted dates.
244     */
245    private SortedSet<Date> readDates(JSONValue json) {
246
247        JSONArray array = null == json ? null : json.isArray();
248        if (null != array) {
249            SortedSet<Date> result = new TreeSet<>();
250            for (int i = 0; i < array.size(); i++) {
251                Date d = readOptionalDate(array.get(i));
252                if (null != d) {
253                    result.add(d);
254                }
255            }
256            return result;
257        }
258        return new TreeSet<>();
259    }
260
261    /**
262     * Read an optional boolean value form a JSON value.
263     * @param val the JSON value that should represent the boolean.
264     * @return the boolean from the JSON or null if reading the boolean fails.
265     */
266    private Boolean readOptionalBoolean(JSONValue val) {
267
268        JSONBoolean b = null == val ? null : val.isBoolean();
269        if (b != null) {
270            return Boolean.valueOf(b.booleanValue());
271        }
272        return null;
273    }
274
275    /**
276     * Read an optional Date value form a JSON value.
277     * @param val the JSON value that should represent the Date as long value in a string.
278     * @return the Date from the JSON or null if reading the date fails.
279     */
280    private Date readOptionalDate(JSONValue val) {
281
282        JSONString str = null == val ? null : val.isString();
283        if (str != null) {
284            try {
285                return new Date(Long.parseLong(str.stringValue()));
286            } catch (@SuppressWarnings("unused") NumberFormatException e) {
287                // do nothing - return the default value
288            }
289        }
290        return null;
291    }
292
293    /**
294     * Read an optional int value form a JSON value.
295     * @param val the JSON value that should represent the int.
296     * @return the int from the JSON or 0 reading the int fails.
297     */
298    private int readOptionalInt(JSONValue val) {
299
300        JSONString str = null == val ? null : val.isString();
301        if (str != null) {
302            try {
303                return Integer.valueOf(str.stringValue()).intValue();
304            } catch (@SuppressWarnings("unused") NumberFormatException e) {
305                // Do nothing, return default value
306            }
307        }
308        return 0;
309    }
310
311    /**
312     * Read an optional month value form a JSON value.
313     * @param val the JSON value that should represent the month.
314     * @return the month from the JSON or null if reading the month fails.
315     */
316    private Month readOptionalMonth(JSONValue val) {
317
318        String str = readOptionalString(val);
319        if (null != str) {
320            try {
321                return Month.valueOf(str);
322            } catch (@SuppressWarnings("unused") IllegalArgumentException e) {
323                // Do nothing -return the default value
324            }
325        }
326        return null;
327    }
328
329    /**
330     * Read an optional string value form a JSON value.
331     * @param val the JSON value that should represent the string.
332     * @return the string from the JSON or null if reading the string fails.
333     */
334    private String readOptionalString(JSONValue val) {
335
336        JSONString str = null == val ? null : val.isString();
337        if (str != null) {
338            return str.stringValue();
339        }
340        return null;
341    }
342
343    /**
344     * Read an optional uuid stored as JSON string.
345     * @param val the JSON value to read the uuid from.
346     * @return the uuid, or <code>null</code> if the uuid can not be read.
347     */
348    private CmsUUID readOptionalUUID(JSONValue val) {
349
350        String id = readOptionalString(val);
351        if (null != id) {
352            try {
353                CmsUUID uuid = CmsUUID.valueOf(id);
354                return uuid;
355            } catch (@SuppressWarnings("unused") NumberFormatException e) {
356                // Do nothing, just return null
357            }
358        }
359        return null;
360    }
361
362    /**
363     * Read pattern information from the provided JSON object.
364     * @param patternJson the JSON object containing the pattern information.
365     */
366    private void readPattern(JSONObject patternJson) {
367
368        setPatternType(readPatternType(patternJson.get(JsonKey.PATTERN_TYPE)));
369        setInterval(readOptionalInt(patternJson.get(JsonKey.PATTERN_INTERVAL)));
370        setWeekDays(readWeekDays(patternJson.get(JsonKey.PATTERN_WEEKDAYS)));
371        setDayOfMonth(readOptionalInt(patternJson.get(JsonKey.PATTERN_DAY_OF_MONTH)));
372        setEveryWorkingDay(readOptionalBoolean(patternJson.get(JsonKey.PATTERN_EVERYWORKINGDAY)));
373        setWeeksOfMonth(readWeeksOfMonth(patternJson.get(JsonKey.PATTERN_WEEKS_OF_MONTH)));
374        setIndividualDates(readDates(patternJson.get(JsonKey.PATTERN_DATES)));
375        setMonth(readOptionalMonth(patternJson.get(JsonKey.PATTERN_MONTH)));
376
377    }
378
379    /**
380     * Reads the pattern type from the provided JSON. Defaults to type NONE.
381     * @param val the JSON object containing the pattern type information.
382     * @return the pattern type.
383     */
384    private PatternType readPatternType(JSONValue val) {
385
386        PatternType patterntype;
387        try {
388            String str = readOptionalString(val);
389            patterntype = PatternType.valueOf(str);
390        } catch (@SuppressWarnings("unused") IllegalArgumentException e) {
391            patterntype = PatternType.NONE;
392        }
393        return patterntype;
394    }
395
396    /**
397     * Read a single weekday from the provided JSON value.
398     * @param val the value to read the week day from.
399     * @return the week day read
400     * @throws IllegalArgumentException thrown if the provided JSON value is not the representation of a week day.
401     */
402    private WeekDay readWeekDay(JSONValue val) throws IllegalArgumentException {
403
404        String str = readOptionalString(val);
405        if (null != str) {
406            return WeekDay.valueOf(str);
407        }
408        throw new IllegalArgumentException();
409    }
410
411    /**
412     * Reads the weekday information.
413     * @param val the JSON object containing the weekday information.
414     * @return the weekdays extracted, defaults to the empty list, if no information is found.
415     */
416    private SortedSet<WeekDay> readWeekDays(JSONValue val) {
417
418        JSONArray array = null == val ? null : val.isArray();
419        if (null != array) {
420            SortedSet<WeekDay> result = new TreeSet<>();
421            for (int i = 0; i < array.size(); i++) {
422                try {
423                    result.add(readWeekDay(array.get(i)));
424                } catch (@SuppressWarnings("unused") IllegalArgumentException e) {
425                    // Just skip
426                }
427            }
428            return result;
429        }
430        return new TreeSet<>();
431    }
432
433    /**
434     * Read "weeks of month" information from JSON.
435     * @param json the JSON where information is read from.
436     * @return the list of weeks read, defaults to the empty list.
437     */
438    private SortedSet<WeekOfMonth> readWeeksOfMonth(JSONValue json) {
439
440        JSONArray array = null == json ? null : json.isArray();
441        if (null != array) {
442            SortedSet<WeekOfMonth> result = new TreeSet<>();
443            for (int i = 0; i < array.size(); i++) {
444                String weekStr = readOptionalString(array.get(i));
445                try {
446                    WeekOfMonth week = WeekOfMonth.valueOf(weekStr);
447                    result.add(week);
448                } catch (@SuppressWarnings("unused") IllegalArgumentException | NullPointerException e) {
449                    // Just skip
450                }
451            }
452            return result;
453        }
454        return new TreeSet<>();
455    }
456
457    /**
458     * Convert a list of objects to a JSON array with the string representations of that objects.
459     * @param list the list of objects.
460     * @return the JSON array with the string representations.
461     */
462    private JSONValue toJsonStringList(Collection<? extends Object> list) {
463
464        if (null != list) {
465            JSONArray array = new JSONArray();
466            for (Object o : list) {
467                array.set(array.size(), new JSONString(o.toString()));
468            }
469            return array;
470        } else {
471            return null;
472        }
473    }
474
475    /**
476     * Try to set the value from the provided Json string.
477     * @param value the value to set.
478     * @throws Exception thrown if parsing fails.
479     */
480    private void tryToSetParsedValue(String value) throws Exception {
481
482        JSONObject json = JSONParser.parseStrict(value).isObject();
483        JSONValue val = json.get(JsonKey.START);
484        setStart(readOptionalDate(val));
485        val = json.get(JsonKey.END);
486        setEnd(readOptionalDate(val));
487        setWholeDay(readOptionalBoolean(json.get(JsonKey.WHOLE_DAY)));
488        JSONObject patternJson = json.get(JsonKey.PATTERN).isObject();
489        readPattern(patternJson);
490        setExceptions(readDates(json.get(JsonKey.EXCEPTIONS)));
491        setSeriesEndDate(readOptionalDate(json.get(JsonKey.SERIES_ENDDATE)));
492        setOccurrences(readOptionalInt(json.get(JsonKey.SERIES_OCCURRENCES)));
493        setDerivedEndType();
494        setCurrentTillEnd(readOptionalBoolean(json.get(JsonKey.CURRENT_TILL_END)));
495        setParentSeriesId(readOptionalUUID(json.get(JsonKey.PARENT_SERIES)));
496
497    }
498}