001 package biweekly.property.marshaller; 002 003 import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; 004 005 import java.util.ArrayList; 006 import java.util.Date; 007 import java.util.List; 008 import java.util.TimeZone; 009 import java.util.regex.Pattern; 010 011 import javax.xml.namespace.QName; 012 013 import org.w3c.dom.Element; 014 015 import biweekly.ICalendar; 016 import biweekly.io.CannotParseException; 017 import biweekly.io.SkipMeException; 018 import biweekly.io.text.ICalWriter; 019 import biweekly.io.xml.XCalElement; 020 import biweekly.parameter.ICalParameters; 021 import biweekly.parameter.Value; 022 import biweekly.property.ICalProperty; 023 import biweekly.util.ICalDateFormatter; 024 import biweekly.util.ISOFormat; 025 026 /* 027 Copyright (c) 2013, Michael Angstadt 028 All rights reserved. 029 030 Redistribution and use in source and binary forms, with or without 031 modification, are permitted provided that the following conditions are met: 032 033 1. Redistributions of source code must retain the above copyright notice, this 034 list of conditions and the following disclaimer. 035 2. Redistributions in binary form must reproduce the above copyright notice, 036 this list of conditions and the following disclaimer in the documentation 037 and/or other materials provided with the distribution. 038 039 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 040 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 041 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 042 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 043 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 044 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 045 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 046 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 047 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 048 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 049 */ 050 051 /** 052 * Base class for iCalendar property marshallers. 053 * @author Michael Angstadt 054 */ 055 public abstract class ICalPropertyMarshaller<T extends ICalProperty> { 056 private static final String NEWLINE = System.getProperty("line.separator"); 057 protected final Class<T> clazz; 058 protected final String propertyName; 059 protected final QName qname; 060 061 /** 062 * Creates a new marshaller. 063 * @param clazz the property class 064 * @param propertyName the property name (e.g. "VERSION") 065 */ 066 public ICalPropertyMarshaller(Class<T> clazz, String propertyName) { 067 this(clazz, propertyName, new QName(XCAL_NS, propertyName.toLowerCase())); 068 } 069 070 /** 071 * Creates a new marshaller. 072 * @param clazz the property class 073 * @param propertyName the property name (e.g. "VERSION") 074 * @param qname the XML element name and namespace (used for xCal documents) 075 */ 076 public ICalPropertyMarshaller(Class<T> clazz, String propertyName, QName qname) { 077 this.clazz = clazz; 078 this.propertyName = propertyName; 079 this.qname = qname; 080 } 081 082 /** 083 * Gets the property class. 084 * @return the property class 085 */ 086 public Class<T> getPropertyClass() { 087 return clazz; 088 } 089 090 /** 091 * Gets the property name. 092 * @return the property name (e.g. "VERSION") 093 */ 094 public String getPropertyName() { 095 return propertyName; 096 } 097 098 /** 099 * Gets this property's local name and namespace for xCal documents. 100 * @return the XML local name and namespace 101 */ 102 public QName getQName() { 103 return qname; 104 } 105 106 /** 107 * Sanitizes a property's parameters (called before the property is 108 * written). Note that a copy of the parameters is returned so that the 109 * property object does not get modified. 110 * @param property the property 111 * @return the sanitized parameters 112 */ 113 public final ICalParameters prepareParameters(T property) { 114 //make a copy because the property should not get modified when it is marshalled 115 ICalParameters copy = new ICalParameters(property.getParameters()); 116 _prepareParameters(property, copy); 117 return copy; 118 } 119 120 /** 121 * Marshals a property's value to a string. 122 * @param property the property 123 * @return the marshalled value 124 * @throws SkipMeException if the property should not be written to the data 125 * stream 126 */ 127 public final String writeText(T property) { 128 return _writeText(property); 129 } 130 131 /** 132 * Marshals a property's value to an XML element (xCal). 133 * @param property the property 134 * @param element the property's XML element 135 * @throws SkipMeException if the property should not be written to the data 136 * stream 137 */ 138 public final void writeXml(T property, Element element) { 139 XCalElement xcalElement = new XCalElement(element); 140 _writeXml(property, xcalElement); 141 } 142 143 /** 144 * Unmarshals a property's value. 145 * @param value the value 146 * @param parameters the property's parameters 147 * @return the unmarshalled property object 148 * @throws CannotParseException if the marshaller could not parse the 149 * property's value 150 * @throws SkipMeException if the property should not be added to the final 151 * {@link ICalendar} object 152 */ 153 public final Result<T> parseText(String value, ICalParameters parameters) { 154 List<String> warnings = new ArrayList<String>(0); 155 T property = _parseText(value, parameters, warnings); 156 property.setParameters(parameters); 157 return new Result<T>(property, warnings); 158 } 159 160 /** 161 * Unmarshals a property's value from an XML document (xCal). 162 * @param element the property's XML element 163 * @param parameters the property's parameters 164 * @return the unmarshalled property object 165 * @throws CannotParseException if the marshaller could not parse the 166 * property's value 167 * @throws SkipMeException if the property should not be added to the final 168 * {@link ICalendar} object 169 */ 170 public final Result<T> parseXml(Element element, ICalParameters parameters) { 171 List<String> warnings = new ArrayList<String>(0); 172 T property = _parseXml(new XCalElement(element), parameters, warnings); 173 property.setParameters(parameters); 174 return new Result<T>(property, warnings); 175 } 176 177 /** 178 * Sanitizes a property's parameters (called before the property is 179 * written). This should be overridden by child classes when required. 180 * @param property the property 181 * @param copy the list of parameters to make modifications to (it is a copy 182 * of the property's parameters) 183 */ 184 protected void _prepareParameters(T property, ICalParameters copy) { 185 //do nothing 186 } 187 188 /** 189 * Marshals a property's value to a string. 190 * @param property the property 191 * @return the marshalled value 192 * @throws SkipMeException if the property should not be written to the data 193 * stream 194 */ 195 protected abstract String _writeText(T property); 196 197 /** 198 * Marshals a property's value to an XML element (xCal). 199 * @param property the property 200 * @param element the XML element 201 * @throws SkipMeException if the property should not be written to the data 202 * stream 203 */ 204 protected void _writeXml(T property, XCalElement element) { 205 String value = writeText(property); 206 Value dataType = property.getParameters().getValue(); 207 if (dataType == null) { 208 element.appendUnknown(value); 209 } else { 210 element.append(dataType, value); 211 } 212 } 213 214 /** 215 * Unmarshals a property's value. 216 * @param value the value 217 * @param parameters the property's parameters 218 * @param warnings allows the programmer to alert the user to any 219 * note-worthy (but non-critical) issues that occurred during the 220 * unmarshalling process 221 * @return the unmarshalled property object 222 * @throws CannotParseException if the marshaller could not parse the 223 * property's value 224 * @throws SkipMeException if the property should not be added to the final 225 * {@link ICalendar} object 226 */ 227 protected abstract T _parseText(String value, ICalParameters parameters, List<String> warnings); 228 229 /** 230 * Unmarshals a property's value from an XML document (xCal). 231 * @param element the property's XML element 232 * @param parameters the property's parameters 233 * @param warnings allows the programmer to alert the user to any 234 * note-worthy (but non-critical) issues that occurred during the 235 * unmarshalling process 236 * @return the unmarshalled property object 237 * @throws CannotParseException if the marshaller could not parse the 238 * property's value 239 * @throws SkipMeException if the property should not be added to the final 240 * {@link ICalendar} object 241 */ 242 protected T _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) { 243 throw new UnsupportedOperationException(); 244 } 245 246 /** 247 * Unescapes all special characters that are escaped with a backslash, as 248 * well as escaped newlines. 249 * @param text the text to unescape 250 * @return the unescaped text 251 */ 252 protected static String unescape(String text) { 253 StringBuilder sb = new StringBuilder(text.length()); 254 boolean escaped = false; 255 for (int i = 0; i < text.length(); i++) { 256 char ch = text.charAt(i); 257 if (escaped) { 258 if (ch == 'n' || ch == 'N') { 259 //newlines appear as "\n" or "\N" (see RFC 2426 p.7) 260 sb.append(NEWLINE); 261 } else { 262 sb.append(ch); 263 } 264 escaped = false; 265 } else if (ch == '\\') { 266 escaped = true; 267 } else { 268 sb.append(ch); 269 } 270 } 271 return sb.toString(); 272 } 273 274 /** 275 * Escapes all special characters within a iCalendar value. 276 * <p> 277 * These characters are: 278 * </p> 279 * <ul> 280 * <li>backslashes (<code>\</code>)</li> 281 * <li>commas (<code>,</code>)</li> 282 * <li>semi-colons (<code>;</code>)</li> 283 * <li>(newlines are escaped by {@link ICalWriter})</li> 284 * </ul> 285 * @param text the text to escape 286 * @return the escaped text 287 */ 288 protected static String escape(String text) { 289 String chars = "\\,;"; 290 for (int i = 0; i < chars.length(); i++) { 291 String ch = chars.substring(i, i + 1); 292 text = text.replace(ch, "\\" + ch); 293 } 294 return text; 295 } 296 297 /** 298 * Splits a string by a delimiter. 299 * @param string the string to split (e.g. "one,two,three") 300 * @param delimiter the delimiter (e.g. ",") 301 * @return the factory object 302 */ 303 protected static Splitter split(String string, String delimiter) { 304 return new Splitter(string, delimiter); 305 } 306 307 /** 308 * Factory class for splitting strings. 309 */ 310 protected static class Splitter { 311 private String string; 312 private String delimiter; 313 private boolean removeEmpties = false; 314 private boolean unescape = false; 315 316 /** 317 * Creates a new splitter object. 318 * @param string the string to split (e.g. "one,two,three") 319 * @param delimiter the delimiter (e.g. ",") 320 */ 321 public Splitter(String string, String delimiter) { 322 this.string = string; 323 this.delimiter = delimiter; 324 } 325 326 /** 327 * Sets whether to remove empty elements. 328 * @param removeEmpties true to remove empty elements, false not to 329 * (default is false) 330 * @return this 331 */ 332 public Splitter removeEmpties(boolean removeEmpties) { 333 this.removeEmpties = removeEmpties; 334 return this; 335 } 336 337 /** 338 * Sets whether to unescape each split string. 339 * @param unescape true to unescape, false not to (default is false) 340 * @return this 341 */ 342 public Splitter unescape(boolean unescape) { 343 this.unescape = unescape; 344 return this; 345 } 346 347 /** 348 * Performs the split operation. 349 * @return the split string 350 */ 351 public String[] split() { 352 //from: http://stackoverflow.com/q/820172">http://stackoverflow.com/q/820172 353 String split[] = string.split("\\s*(?<!\\\\)" + Pattern.quote(delimiter) + "\\s*", -1); 354 355 List<String> list = new ArrayList<String>(split.length); 356 for (String s : split) { 357 if (s.length() == 0 && removeEmpties) { 358 continue; 359 } 360 361 if (unescape) { 362 s = ICalPropertyMarshaller.unescape(s); 363 } 364 365 list.add(s); 366 } 367 368 return list.toArray(new String[0]); 369 } 370 } 371 372 /** 373 * Parses a comma-separated list of values. 374 * @param str the string to parse (e.g. "one,two,th\,ree") 375 * @return the parsed values 376 */ 377 protected static String[] parseList(String str) { 378 return split(str, ",").removeEmpties(true).unescape(true).split(); 379 } 380 381 /** 382 * Parses a component value. 383 * @param str the string to parse (e.g. "one;two,three;four") 384 * @return the parsed values 385 */ 386 protected static String[][] parseComponent(String str) { 387 String split[] = split(str, ";").split(); 388 String ret[][] = new String[split.length][]; 389 int i = 0; 390 for (String s : split) { 391 String split2[] = parseList(s); 392 ret[i++] = split2; 393 } 394 return ret; 395 } 396 397 /** 398 * Parses a date string. 399 * @param value the date string 400 * @return the factory object 401 */ 402 protected static DateParser date(String value) { 403 return new DateParser(value); 404 } 405 406 /** 407 * Formats a {@link Date} object as a string. 408 * @param date the date 409 * @return the factory object 410 */ 411 protected static DateWriter date(Date date) { 412 return new DateWriter(date); 413 } 414 415 /** 416 * Factory class for parsing dates. 417 */ 418 protected static class DateParser { 419 private String value; 420 private TimeZone timezone; 421 422 /** 423 * Creates a new date writer object. 424 * @param value the date string to parse 425 */ 426 public DateParser(String value) { 427 this.value = value; 428 } 429 430 /** 431 * Sets the ID of the timezone to parse the date as (TZID parameter 432 * value). If the ID does not contain a "/" character, it will be 433 * ignored. 434 * @param timezoneId the timezone ID 435 * @return this 436 */ 437 public DateParser tzid(String timezoneId) { 438 return tzid(timezoneId, null); 439 } 440 441 /** 442 * Sets the ID of the timezone to parse the date as (TZID parameter 443 * value). If the ID does not contain a "/" character, it will be 444 * ignored. If the ID is invalid, the date will be formatted according 445 * to the JVM's default timezone and a warning message will be added to 446 * the provided warnings list. 447 * @param timezoneId the timezone ID 448 * @param warnings if the ID is not recognized, a warning message will 449 * be added to this list 450 * @return this 451 */ 452 public DateParser tzid(String timezoneId, List<String> warnings) { 453 if (timezoneId == null) { 454 timezone = null; 455 return this; 456 } 457 458 if (timezoneId.contains("/")) { 459 timezone = ICalDateFormatter.parseTimeZoneId(timezoneId); 460 if (timezone == null) { 461 timezone = TimeZone.getDefault(); 462 if (warnings != null) { 463 warnings.add("Timezone ID not recognized, parsing with default timezone instead: " + timezoneId); 464 } 465 } 466 } else { 467 //TODO support VTIMEZONE 468 } 469 return this; 470 } 471 472 /** 473 * Sets the timezone to parse the date as. 474 * @param timezone the timezone 475 * @return this 476 */ 477 public DateParser tz(TimeZone timezone) { 478 this.timezone = timezone; 479 return this; 480 } 481 482 /** 483 * Parses the date string. 484 * @return the parsed date 485 * @throws IllegalArgumentException if the date string is invalid 486 */ 487 public Date parse() { 488 return ICalDateFormatter.parse(value, timezone); 489 } 490 } 491 492 /** 493 * Factory class for writing dates. 494 */ 495 protected static class DateWriter { 496 private Date date; 497 private boolean hasTime = true; 498 private TimeZone timezone; 499 private boolean extended = false; 500 501 /** 502 * Creates a new date writer object. 503 * @param date the date to format 504 */ 505 public DateWriter(Date date) { 506 this.date = date; 507 } 508 509 /** 510 * Sets whether to output the date's time component. 511 * @param hasTime true include the time, false if it's strictly a date 512 * (defaults to "true") 513 * @return this 514 */ 515 public DateWriter time(boolean hasTime) { 516 this.hasTime = hasTime; 517 return this; 518 } 519 520 /** 521 * Sets the ID of the timezone to format the date as (TZID parameter 522 * value). If the ID does not contain a "/" character, it will be 523 * ignored. If the ID is invalid, the date will be formatted according 524 * to the JVM's default timezone. If no timezone is specified, the date 525 * will be formatted as UTC. 526 * @param timezoneId the timezone ID 527 * @return this 528 */ 529 public DateWriter tzid(String timezoneId) { 530 if (timezoneId == null) { 531 timezone = null; 532 return this; 533 } 534 535 if (timezoneId.contains("/")) { 536 timezone = ICalDateFormatter.parseTimeZoneId(timezoneId); 537 } else { 538 //TODO support VTIMEZONE 539 timezone = TimeZone.getDefault(); 540 } 541 return this; 542 } 543 544 /** 545 * Sets the timezone to format the date as. If no timezone is specified, 546 * the date will be formatted as UTC. 547 * @param timezone the timezone 548 * @return this 549 */ 550 public DateWriter tz(TimeZone timezone) { 551 this.timezone = timezone; 552 return this; 553 } 554 555 /** 556 * Sets whether to use extended format or basic. 557 * @param extended true to use extended format, false to use basic 558 * (defaults to "false") 559 * @return this 560 */ 561 public DateWriter extended(boolean extended) { 562 this.extended = extended; 563 return this; 564 } 565 566 /** 567 * Creates the date string. 568 * @return the date string 569 */ 570 public String write() { 571 ISOFormat format; 572 if (hasTime) { 573 if (timezone == null) { 574 format = extended ? ISOFormat.UTC_TIME_EXTENDED : ISOFormat.UTC_TIME_BASIC; 575 } else { 576 format = extended ? ISOFormat.TIME_EXTENDED_WITHOUT_TZ : ISOFormat.TIME_BASIC_WITHOUT_TZ; 577 } 578 } else { 579 format = extended ? ISOFormat.DATE_EXTENDED : ISOFormat.DATE_BASIC; 580 } 581 582 return ICalDateFormatter.format(date, format, timezone); 583 } 584 } 585 586 /** 587 * Represents the result of a marshal or unmarshal operation. 588 * @author Michael Angstadt 589 * @param <T> the marshalled/unmarshalled value (e.g. "String" if a property 590 * was marshalled) 591 */ 592 public static class Result<T> { 593 private final T value; 594 private final List<String> warnings; 595 596 /** 597 * Creates a new result. 598 * @param value the value 599 * @param warnings the warnings 600 */ 601 public Result(T value, List<String> warnings) { 602 this.value = value; 603 this.warnings = warnings; 604 } 605 606 /** 607 * Gets the warnings. 608 * @return the warnings 609 */ 610 public List<String> getWarnings() { 611 return warnings; 612 } 613 614 /** 615 * Gets the value. 616 * @return the value 617 */ 618 public T getValue() { 619 return value; 620 } 621 } 622 }