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}