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}