src/net/fortuna/ical4j/data/HCalendarParser.java

Tue, 10 Feb 2015 19:58:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 10 Feb 2015 19:58:00 +0100
changeset 4
45d57ecba757
parent 0
fb9019fb1bf7
permissions
-rwxr-xr-x

Upgrade the upgraded ical4j component to use org.apache.commons.lang3.

michael@0 1 /**
michael@0 2 * Copyright (c) 2012, Ben Fortuna
michael@0 3 * All rights reserved.
michael@0 4 *
michael@0 5 * Redistribution and use in source and binary forms, with or without
michael@0 6 * modification, are permitted provided that the following conditions
michael@0 7 * are met:
michael@0 8 *
michael@0 9 * o Redistributions of source code must retain the above copyright
michael@0 10 * notice, this list of conditions and the following disclaimer.
michael@0 11 *
michael@0 12 * o Redistributions in binary form must reproduce the above copyright
michael@0 13 * notice, this list of conditions and the following disclaimer in the
michael@0 14 * documentation and/or other materials provided with the distribution.
michael@0 15 *
michael@0 16 * o Neither the name of Ben Fortuna nor the names of any other contributors
michael@0 17 * may be used to endorse or promote products derived from this software
michael@0 18 * without specific prior written permission.
michael@0 19 *
michael@0 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
michael@0 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
michael@0 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
michael@0 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
michael@0 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
michael@0 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
michael@0 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
michael@0 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
michael@0 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
michael@0 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
michael@0 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
michael@0 31 */
michael@0 32 package net.fortuna.ical4j.data;
michael@0 33
michael@0 34 import java.io.IOException;
michael@0 35 import java.io.InputStream;
michael@0 36 import java.io.Reader;
michael@0 37 import java.net.URISyntaxException;
michael@0 38 import java.text.ParseException;
michael@0 39 import java.text.SimpleDateFormat;
michael@0 40 import java.util.ArrayList;
michael@0 41 import java.util.Iterator;
michael@0 42 import java.util.List;
michael@0 43
michael@0 44 import javax.xml.XMLConstants;
michael@0 45 import javax.xml.parsers.DocumentBuilderFactory;
michael@0 46 import javax.xml.parsers.ParserConfigurationException;
michael@0 47 import javax.xml.xpath.XPath;
michael@0 48 import javax.xml.xpath.XPathConstants;
michael@0 49 import javax.xml.xpath.XPathException;
michael@0 50 import javax.xml.xpath.XPathExpression;
michael@0 51 import javax.xml.xpath.XPathFactory;
michael@0 52
michael@0 53 import net.fortuna.ical4j.model.CalendarException;
michael@0 54 import net.fortuna.ical4j.model.Component;
michael@0 55 import net.fortuna.ical4j.model.Date;
michael@0 56 import net.fortuna.ical4j.model.DateTime;
michael@0 57 import net.fortuna.ical4j.model.Parameter;
michael@0 58 import net.fortuna.ical4j.model.Property;
michael@0 59 import net.fortuna.ical4j.model.parameter.Value;
michael@0 60 import net.fortuna.ical4j.model.property.Version;
michael@0 61
michael@4 62 import org.apache.commons.lang3.StringUtils;
michael@0 63 import org.apache.commons.logging.Log;
michael@0 64 import org.apache.commons.logging.LogFactory;
michael@0 65 import org.w3c.dom.DOMException;
michael@0 66 import org.w3c.dom.Document;
michael@0 67 import org.w3c.dom.Element;
michael@0 68 import org.w3c.dom.Node;
michael@0 69 import org.w3c.dom.NodeList;
michael@0 70 import org.xml.sax.InputSource;
michael@0 71 import org.xml.sax.SAXException;
michael@0 72 import org.xml.sax.SAXParseException;
michael@0 73
michael@0 74 /**
michael@0 75 * A {@link CalendarParser} that parses XHTML documents that include calendar data marked up with the hCalendar
michael@0 76 * microformat.
michael@0 77 * <p>
michael@0 78 * The parser treats the entire document as a single "vcalendar" context, ignoring any <code>vcalendar</code> elements
michael@0 79 * and adding all components in the document to a single generated calendar.
michael@0 80 * </p>
michael@0 81 * <p>
michael@0 82 * Since hCalendar does not include product information, the <code>PRODID</code> property is omitted from the generated
michael@0 83 * calendar. The hCalendar profile is supposed to define the iCalendar version that it represents, but it does not, so
michael@0 84 * version 2.0 is assumed.
michael@0 85 * </p>
michael@0 86 * <h3>Supported Components</h3>
michael@0 87 * <p>
michael@0 88 * This parser recognizes only "vevent" components.
michael@0 89 * </p>
michael@0 90 * <h3>Supported Properties</h3>
michael@0 91 * <p>
michael@0 92 * This parser recognizes the following properties:
michael@0 93 * </p>
michael@0 94 * <ul>
michael@0 95 * <li>"dtstart"</li>
michael@0 96 * <li>"dtend"</li>
michael@0 97 * <li>"duration"</li>
michael@0 98 * <li>"summary"</li>
michael@0 99 * <li>"uid"</li>
michael@0 100 * <li>"dtstamp"</li>
michael@0 101 * <li>"category"</li>
michael@0 102 * <li>"location"</li>
michael@0 103 * <li>"url"</li>
michael@0 104 * <li>"description"</li>
michael@0 105 * <li>"last-modified"</li>
michael@0 106 * <li>"status"</li>
michael@0 107 * <li>"class"</li>
michael@0 108 * <li>"attendee"</li>
michael@0 109 * <li>"contact"</li>
michael@0 110 * <li>"organizer"</li>
michael@0 111 * </ul>
michael@0 112 * <p>
michael@0 113 * hCalendar allows for some properties to be represented by nested microformat records, including hCard, adr and geo.
michael@0 114 * This parser does not recognize these records. It simply accumulates the text content of any child elements of the
michael@0 115 * property element and uses the resulting string as the property value.
michael@0 116 * </p>
michael@0 117 * <h4>Date and Date-Time Properties</h4>
michael@0 118 * <p>
michael@0 119 * hCalendar date-time values are formatted according to RFC 3339. There is no representation in this specification for
michael@0 120 * time zone ids. All date-times are specified either in UTC or with an offset that can be used to convert the local
michael@0 121 * time into UTC. Neither does hCal provide a reprsentation for floating date-times. Therefore, all date-time values
michael@0 122 * produced by this parser are in UTC.
michael@0 123 * </p>
michael@0 124 * <p>
michael@0 125 * Some examples in the wild provide date and date-time values in iCalendar format rather than RFC 3339 format. Although
michael@0 126 * not technically legal according to spec, these values are accepted. In this case, floating date-times are produced by
michael@0 127 * the parser.
michael@0 128 * </p>
michael@0 129 * <h3>Supported Parameters</h3>
michael@0 130 * <p>
michael@0 131 * hCalendar does not define attributes, nested elements or other information elements representing parameter data.
michael@0 132 * Therefore, this parser does not set any property parameters except as implied by property value data (e.g.
michael@0 133 * VALUE=DATE-TIME or VALUE=DATE for date-time properties).
michael@0 134 * </p>
michael@0 135 */
michael@0 136 public class HCalendarParser implements CalendarParser {
michael@0 137
michael@0 138 private static final Log LOG = LogFactory.getLog(HCalendarParser.class);
michael@0 139
michael@0 140 private static final DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
michael@0 141 private static final XPath XPATH = XPathFactory.newInstance().newXPath();
michael@0 142 private static final XPathExpression XPATH_METHOD;
michael@0 143 private static final XPathExpression XPATH_VEVENTS;
michael@0 144 private static final XPathExpression XPATH_DTSTART;
michael@0 145 private static final XPathExpression XPATH_DTEND;
michael@0 146 private static final XPathExpression XPATH_DURATION;
michael@0 147 private static final XPathExpression XPATH_SUMMARY;
michael@0 148 private static final XPathExpression XPATH_UID;
michael@0 149 private static final XPathExpression XPATH_DTSTAMP;
michael@0 150 private static final XPathExpression XPATH_CATEGORY;
michael@0 151 private static final XPathExpression XPATH_LOCATION;
michael@0 152 private static final XPathExpression XPATH_URL;
michael@0 153 private static final XPathExpression XPATH_DESCRIPTION;
michael@0 154 private static final XPathExpression XPATH_LAST_MODIFIED;
michael@0 155 private static final XPathExpression XPATH_STATUS;
michael@0 156 private static final XPathExpression XPATH_CLASS;
michael@0 157 private static final XPathExpression XPATH_ATTENDEE;
michael@0 158 private static final XPathExpression XPATH_CONTACT;
michael@0 159 private static final XPathExpression XPATH_ORGANIZER;
michael@0 160 private static final XPathExpression XPATH_SEQUENCE;
michael@0 161 private static final XPathExpression XPATH_ATTACH;
michael@0 162 private static final String HCAL_DATE_PATTERN = "yyyy-MM-dd";
michael@0 163 private static final SimpleDateFormat HCAL_DATE_FORMAT = new SimpleDateFormat(HCAL_DATE_PATTERN);
michael@0 164 private static final String HCAL_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssz";
michael@0 165 private static final SimpleDateFormat HCAL_DATE_TIME_FORMAT = new SimpleDateFormat(HCAL_DATE_TIME_PATTERN);
michael@0 166
michael@0 167 static {
michael@0 168 BUILDER_FACTORY.setNamespaceAware(true);
michael@0 169 BUILDER_FACTORY.setIgnoringComments(true);
michael@0 170
michael@0 171 XPATH_METHOD = compileExpression("//*[contains(@class, 'method')]");
michael@0 172 XPATH_VEVENTS = compileExpression("//*[contains(@class, 'vevent')]");
michael@0 173 XPATH_DTSTART = compileExpression(".//*[contains(@class, 'dtstart')]");
michael@0 174 XPATH_DTEND = compileExpression(".//*[contains(@class, 'dtend')]");
michael@0 175 XPATH_DURATION = compileExpression(".//*[contains(@class, 'duration')]");
michael@0 176 XPATH_SUMMARY = compileExpression(".//*[contains(@class, 'summary')]");
michael@0 177 XPATH_UID = compileExpression(".//*[contains(@class, 'uid')]");
michael@0 178 XPATH_DTSTAMP = compileExpression(".//*[contains(@class, 'dtstamp')]");
michael@0 179 XPATH_CATEGORY = compileExpression(".//*[contains(@class, 'category')]");
michael@0 180 XPATH_LOCATION = compileExpression(".//*[contains(@class, 'location')]");
michael@0 181 XPATH_URL = compileExpression(".//*[contains(@class, 'url')]");
michael@0 182 XPATH_DESCRIPTION = compileExpression(".//*[contains(@class, 'description')]");
michael@0 183 XPATH_LAST_MODIFIED = compileExpression(".//*[contains(@class, 'last-modified')]");
michael@0 184 XPATH_STATUS = compileExpression(".//*[contains(@class, 'status')]");
michael@0 185 XPATH_CLASS = compileExpression(".//*[contains(@class, 'class')]");
michael@0 186 XPATH_ATTENDEE = compileExpression(".//*[contains(@class, 'attendee')]");
michael@0 187 XPATH_CONTACT = compileExpression(".//*[contains(@class, 'contact')]");
michael@0 188 XPATH_ORGANIZER = compileExpression(".//*[contains(@class, 'organizer')]");
michael@0 189 XPATH_SEQUENCE = compileExpression(".//*[contains(@class, 'sequence')]");
michael@0 190 XPATH_ATTACH = compileExpression(".//*[contains(@class, 'attach')]");
michael@0 191 }
michael@0 192
michael@0 193 private static XPathExpression compileExpression(String expr) {
michael@0 194 try {
michael@0 195 return XPATH.compile(expr);
michael@0 196 } catch (XPathException e) {
michael@0 197 throw new CalendarException(e);
michael@0 198 }
michael@0 199 }
michael@0 200
michael@0 201 /**
michael@0 202 * {@inheritDoc}
michael@0 203 */
michael@0 204 public void parse(InputStream in, ContentHandler handler) throws IOException, ParserException {
michael@0 205 parse(new InputSource(in), handler);
michael@0 206 }
michael@0 207
michael@0 208 /**
michael@0 209 * {@inheritDoc}
michael@0 210 */
michael@0 211 public void parse(Reader in, ContentHandler handler) throws IOException, ParserException {
michael@0 212 parse(new InputSource(in), handler);
michael@0 213 }
michael@0 214
michael@0 215 private void parse(InputSource in, ContentHandler handler) throws IOException, ParserException {
michael@0 216 try {
michael@0 217 Document d = BUILDER_FACTORY.newDocumentBuilder().parse(in);
michael@0 218 buildCalendar(d, handler);
michael@0 219 } catch (ParserConfigurationException e) {
michael@0 220 throw new CalendarException(e);
michael@0 221 } catch (SAXException e) {
michael@0 222 if (e instanceof SAXParseException) {
michael@0 223 SAXParseException pe = (SAXParseException) e;
michael@0 224 throw new ParserException("Could not parse XML", pe.getLineNumber(), e);
michael@0 225 }
michael@0 226 throw new ParserException(e.getMessage(), -1, e);
michael@0 227 }
michael@0 228 }
michael@0 229
michael@0 230 private static NodeList findNodes(XPathExpression expr, Object context) throws ParserException {
michael@0 231 try {
michael@0 232 return (NodeList) expr.evaluate(context, XPathConstants.NODESET);
michael@0 233 } catch (XPathException e) {
michael@0 234 throw new ParserException("Unable to find nodes", -1, e);
michael@0 235 }
michael@0 236 }
michael@0 237
michael@0 238 private static Node findNode(XPathExpression expr, Object context) throws ParserException {
michael@0 239 try {
michael@0 240 return (Node) expr.evaluate(context, XPathConstants.NODE);
michael@0 241 } catch (XPathException e) {
michael@0 242 throw new ParserException("Unable to find node", -1, e);
michael@0 243 }
michael@0 244 }
michael@0 245
michael@0 246 private static List findElements(XPathExpression expr, Object context) throws ParserException {
michael@0 247 NodeList nodes = findNodes(expr, context);
michael@0 248 ArrayList elements = new ArrayList();
michael@0 249 for (int i = 0; i < nodes.getLength(); i++) {
michael@0 250 Node n = nodes.item(i);
michael@0 251 if (n instanceof Element)
michael@0 252 elements.add((Element) n);
michael@0 253 }
michael@0 254 return elements;
michael@0 255 }
michael@0 256
michael@0 257 private static Element findElement(XPathExpression expr, Object context) throws ParserException {
michael@0 258 Node n = findNode(expr, context);
michael@0 259 if (n == null || (!(n instanceof Element)))
michael@0 260 return null;
michael@0 261 return (Element) n;
michael@0 262 }
michael@0 263
michael@0 264 private static String getTextContent(Element element) throws ParserException {
michael@0 265 try {
michael@0 266 String content = element.getFirstChild().getNodeValue();
michael@0 267 if (content != null) {
michael@0 268 return content.trim().replaceAll("\\s+", " ");
michael@0 269 }
michael@0 270 return content;
michael@0 271 } catch (DOMException e) {
michael@0 272 throw new ParserException("Unable to get text content for element " + element.getNodeName(), -1, e);
michael@0 273 }
michael@0 274 }
michael@0 275
michael@0 276 private void buildCalendar(Document d, ContentHandler handler) throws ParserException {
michael@0 277 // "The root class name for hCalendar is "vcalendar". An element with a
michael@0 278 // class name of "vcalendar" is itself called an hCalendar.
michael@0 279 //
michael@0 280 // The root class name for events is "vevent". An element with a class
michael@0 281 // name of "vevent" is itself called an hCalender event.
michael@0 282 //
michael@0 283 // For authoring convenience, both "vevent" and "vcalendar" are
michael@0 284 // treated as root class names for parsing purposes. If a document
michael@0 285 // contains elements with class name "vevent" but not "vcalendar", the
michael@0 286 // entire document has an implied "vcalendar" context."
michael@0 287
michael@0 288 // XXX: We assume that the entire document has a single vcalendar
michael@0 289 // context. It is possible that the document contains more than one
michael@0 290 // vcalendar element. In this case, we should probably only process
michael@0 291 // that element and log a warning about skipping the others.
michael@0 292
michael@0 293 if (LOG.isDebugEnabled())
michael@0 294 LOG.debug("Building calendar");
michael@0 295
michael@0 296 handler.startCalendar();
michael@0 297
michael@0 298 // no PRODID, as the using application should set that itself
michael@0 299
michael@0 300 handler.startProperty(Property.VERSION);
michael@0 301 try {
michael@0 302 handler.propertyValue(Version.VERSION_2_0.getValue());
michael@0 303 } catch (Exception e) {
michael@0 304 }
michael@0 305 ;
michael@0 306 handler.endProperty(Property.VERSION);
michael@0 307
michael@0 308 Element method = findElement(XPATH_METHOD, d);
michael@0 309 if (method != null) {
michael@0 310 buildProperty(method, Property.METHOD, handler);
michael@0 311 }
michael@0 312
michael@0 313 List vevents = findElements(XPATH_VEVENTS, d);
michael@0 314 for (Iterator i = vevents.iterator(); i.hasNext();) {
michael@0 315 Element vevent = (Element) i.next();
michael@0 316 buildEvent(vevent, handler);
michael@0 317 }
michael@0 318
michael@0 319 // XXX: support other "first class components": vjournal, vtodo,
michael@0 320 // vfreebusy, vavailability, vvenue
michael@0 321
michael@0 322 handler.endCalendar();
michael@0 323 }
michael@0 324
michael@0 325 private void buildEvent(Element element, ContentHandler handler) throws ParserException {
michael@0 326 if (LOG.isDebugEnabled())
michael@0 327 LOG.debug("Building event");
michael@0 328
michael@0 329 handler.startComponent(Component.VEVENT);
michael@0 330
michael@0 331 buildProperty(findElement(XPATH_DTSTART, element), Property.DTSTART, handler);
michael@0 332 buildProperty(findElement(XPATH_DTEND, element), Property.DTEND, handler);
michael@0 333 buildProperty(findElement(XPATH_DURATION, element), Property.DURATION, handler);
michael@0 334 buildProperty(findElement(XPATH_SUMMARY, element), Property.SUMMARY, handler);
michael@0 335 buildProperty(findElement(XPATH_UID, element), Property.UID, handler);
michael@0 336 buildProperty(findElement(XPATH_DTSTAMP, element), Property.DTSTAMP, handler);
michael@0 337 List categories = findElements(XPATH_CATEGORY, element);
michael@0 338 for (Iterator i = categories.iterator(); i.hasNext();) {
michael@0 339 Element category = (Element) i.next();
michael@0 340 buildProperty(category, Property.CATEGORIES, handler);
michael@0 341 }
michael@0 342 buildProperty(findElement(XPATH_LOCATION, element), Property.LOCATION, handler);
michael@0 343 buildProperty(findElement(XPATH_URL, element), Property.URL, handler);
michael@0 344 buildProperty(findElement(XPATH_DESCRIPTION, element), Property.DESCRIPTION, handler);
michael@0 345 buildProperty(findElement(XPATH_LAST_MODIFIED, element), Property.LAST_MODIFIED, handler);
michael@0 346 buildProperty(findElement(XPATH_STATUS, element), Property.STATUS, handler);
michael@0 347 buildProperty(findElement(XPATH_CLASS, element), Property.CLASS, handler);
michael@0 348 List attendees = findElements(XPATH_ATTENDEE, element);
michael@0 349 for (Iterator i = attendees.iterator(); i.hasNext();) {
michael@0 350 Element attendee = (Element) i.next();
michael@0 351 buildProperty(attendee, Property.ATTENDEE, handler);
michael@0 352 }
michael@0 353 buildProperty(findElement(XPATH_CONTACT, element), Property.CONTACT, handler);
michael@0 354 buildProperty(findElement(XPATH_ORGANIZER, element), Property.ORGANIZER, handler);
michael@0 355 buildProperty(findElement(XPATH_SEQUENCE, element), Property.SEQUENCE, handler);
michael@0 356 buildProperty(findElement(XPATH_ATTACH, element), Property.ATTACH, handler);
michael@0 357
michael@0 358 handler.endComponent(Component.VEVENT);
michael@0 359 }
michael@0 360
michael@0 361 private void buildProperty(Element element, String propName, ContentHandler handler) throws ParserException {
michael@0 362 if (element == null)
michael@0 363 return;
michael@0 364
michael@0 365 if (LOG.isDebugEnabled())
michael@0 366 LOG.debug("Building property " + propName);
michael@0 367
michael@0 368 String className = className(propName);
michael@0 369 String elementName = element.getLocalName().toLowerCase();
michael@0 370
michael@0 371 String value = null;
michael@0 372 if (elementName.equals("abbr")) {
michael@0 373 // "If an <abbr> element is used for a property, then the 'title'
michael@0 374 // attribute of the <abbr> element is the value of the property,
michael@0 375 // instead of the contents of the element, which instead provide a
michael@0 376 // human presentable version of the value."
michael@0 377 value = element.getAttribute("title");
michael@0 378 if (StringUtils.isBlank(value))
michael@0 379 throw new ParserException("Abbr element '" + className + "' requires a non-empty title", -1);
michael@0 380 if (LOG.isDebugEnabled())
michael@0 381 LOG.debug("Setting value '" + value + "' from title attribute");
michael@0 382 } else if (isHeaderElement(elementName)) {
michael@0 383 // try title first. if that's not set, fall back to text content.
michael@0 384 value = element.getAttribute("title");
michael@0 385 if (!StringUtils.isBlank(value)) {
michael@0 386 if (LOG.isDebugEnabled())
michael@0 387 LOG.debug("Setting value '" + value + "' from title attribute");
michael@0 388 } else {
michael@0 389 value = getTextContent(element);
michael@0 390 if (LOG.isDebugEnabled())
michael@0 391 LOG.debug("Setting value '" + value + "' from text content");
michael@0 392 }
michael@0 393 } else if (elementName.equals("a") && isUrlProperty(propName)) {
michael@0 394 value = element.getAttribute("href");
michael@0 395 if (StringUtils.isBlank(value))
michael@0 396 throw new ParserException("A element '" + className + "' requires a non-empty href", -1);
michael@0 397 if (LOG.isDebugEnabled())
michael@0 398 LOG.debug("Setting value '" + value + "' from href attribute");
michael@0 399 } else if (elementName.equals("img")) {
michael@0 400 if (isUrlProperty(propName)) {
michael@0 401 value = element.getAttribute("src");
michael@0 402 if (StringUtils.isBlank(value))
michael@0 403 throw new ParserException("Img element '" + className + "' requires a non-empty src", -1);
michael@0 404 if (LOG.isDebugEnabled())
michael@0 405 LOG.debug("Setting value '" + value + "' from src attribute");
michael@0 406 } else {
michael@0 407 value = element.getAttribute("alt");
michael@0 408 if (StringUtils.isBlank(value))
michael@0 409 throw new ParserException("Img element '" + className + "' requires a non-empty alt", -1);
michael@0 410 if (LOG.isDebugEnabled())
michael@0 411 LOG.debug("Setting value '" + value + "' from alt attribute");
michael@0 412 }
michael@0 413 } else {
michael@0 414 value = getTextContent(element);
michael@0 415 if (!StringUtils.isBlank(value)) {
michael@0 416 if (LOG.isDebugEnabled())
michael@0 417 LOG.debug("Setting value '" + value + "' from text content");
michael@0 418 }
michael@0 419 }
michael@0 420
michael@0 421 if (StringUtils.isBlank(value)) {
michael@0 422 if (LOG.isDebugEnabled())
michael@0 423 LOG.debug("Skipping property with empty value");
michael@0 424 return;
michael@0 425 }
michael@0 426
michael@0 427 handler.startProperty(propName);
michael@0 428
michael@0 429 // if it's a date property, we have to convert from the
michael@0 430 // hCalendar-formatted date (RFC 3339) to an iCalendar-formatted date
michael@0 431 if (isDateProperty(propName)) {
michael@0 432 try {
michael@0 433 Date date = icalDate(value);
michael@0 434 value = date.toString();
michael@0 435
michael@0 436 if (!(date instanceof DateTime))
michael@0 437 try {
michael@0 438 handler.parameter(Parameter.VALUE, Value.DATE.getValue());
michael@0 439 } catch (Exception e) {
michael@0 440 }
michael@0 441 } catch (ParseException e) {
michael@0 442 throw new ParserException("Malformed date value for element '" + className + "'", -1, e);
michael@0 443 }
michael@0 444 }
michael@0 445
michael@0 446 if (isTextProperty(propName)) {
michael@0 447 String lang = element.getAttributeNS(XMLConstants.XML_NS_URI, "lang");
michael@0 448 if (!StringUtils.isBlank(lang))
michael@0 449 try {
michael@0 450 handler.parameter(Parameter.LANGUAGE, lang);
michael@0 451 } catch (Exception e) {
michael@0 452 }
michael@0 453 }
michael@0 454
michael@0 455 // XXX: other parameters?
michael@0 456
michael@0 457 try {
michael@0 458 handler.propertyValue(value);
michael@0 459 } catch (URISyntaxException e) {
michael@0 460 throw new ParserException("Malformed URI value for element '" + className + "'", -1, e);
michael@0 461 } catch (ParseException e) {
michael@0 462 throw new ParserException("Malformed value for element '" + className + "'", -1, e);
michael@0 463 } catch (IOException e) {
michael@0 464 throw new CalendarException(e);
michael@0 465 }
michael@0 466
michael@0 467 handler.endProperty(propName);
michael@0 468 }
michael@0 469
michael@0 470 // "The basic format of hCalendar is to use iCalendar object/property
michael@0 471 // names in lower-case for class names ..."
michael@0 472 /*
michael@0 473 * private static String _icalName(Element element) { return element.getAttribute("class").toUpperCase(); }
michael@0 474 */
michael@0 475
michael@0 476 private static String className(String propName) {
michael@0 477 return propName.toLowerCase();
michael@0 478 }
michael@0 479
michael@0 480 private static boolean isHeaderElement(String name) {
michael@0 481 return (name.equals("h1") || name.equals("h2") || name.equals("h3")
michael@0 482 || name.equals("h4") || name.equals("h5") || name
michael@0 483 .equals("h6"));
michael@0 484 }
michael@0 485
michael@0 486 private static boolean isDateProperty(String name) {
michael@0 487 return (name.equals(Property.DTSTART) || name.equals(Property.DTEND) || name.equals(Property.DTSTAMP) || name
michael@0 488 .equals(Property.LAST_MODIFIED));
michael@0 489 }
michael@0 490
michael@0 491 private static boolean isUrlProperty(String name) {
michael@0 492 return (name.equals(Property.URL));
michael@0 493 }
michael@0 494
michael@0 495 private static boolean isTextProperty(String name) {
michael@0 496 return (name.equals(Property.SUMMARY) || name.equals(Property.LOCATION) || name.equals(Property.CATEGORIES)
michael@0 497 || name.equals(Property.DESCRIPTION) || name.equals(Property.ATTENDEE)
michael@0 498 || name.equals(Property.CONTACT) || name
michael@0 499 .equals(Property.ORGANIZER));
michael@0 500 }
michael@0 501
michael@0 502 private static Date icalDate(String original) throws ParseException {
michael@0 503 // in the real world, some generators use iCalendar formatted
michael@0 504 // dates and date-times, so try parsing those formats first before
michael@0 505 // going to RFC 3339 formats
michael@0 506
michael@0 507 if (original.indexOf('T') == -1) {
michael@0 508 // date-only
michael@0 509 try {
michael@0 510 // for some reason Date's pattern matches yyyy-MM-dd, so
michael@0 511 // don't check it if we find -
michael@0 512 if (original.indexOf('-') == -1)
michael@0 513 return new Date(original);
michael@0 514 } catch (Exception e) {
michael@0 515 }
michael@0 516 return new Date(HCAL_DATE_FORMAT.parse(original));
michael@0 517 }
michael@0 518
michael@0 519 try {
michael@0 520 return new DateTime(original);
michael@0 521 } catch (Exception e) {
michael@0 522 }
michael@0 523
michael@0 524 // the date-time value can represent its time zone in a few different
michael@0 525 // ways. we have to normalize those to match our pattern.
michael@0 526
michael@0 527 String normalized = null;
michael@0 528
michael@0 529 if (LOG.isDebugEnabled())
michael@0 530 LOG.debug("normalizing date-time " + original);
michael@0 531
michael@0 532 // 2002-10-09T19:00:00Z
michael@0 533 if (original.charAt(original.length() - 1) == 'Z') {
michael@0 534 normalized = original.replaceAll("Z", "GMT-00:00");
michael@0 535 }
michael@0 536 // 2002-10-10T00:00:00+05:00
michael@0 537 else if (original.indexOf("GMT") == -1
michael@0 538 && (original.charAt(original.length() - 6) == '+' || original.charAt(original.length() - 6) == '-')) {
michael@0 539 String tzId = "GMT" + original.substring(original.length() - 6);
michael@0 540 normalized = original.substring(0, original.length() - 6) + tzId;
michael@0 541 } else {
michael@0 542 // 2002-10-10T00:00:00GMT+05:00
michael@0 543 normalized = original;
michael@0 544 }
michael@0 545
michael@0 546 DateTime dt = new DateTime(HCAL_DATE_TIME_FORMAT.parse(normalized));
michael@0 547
michael@0 548 // hCalendar does not specify a representation for timezone ids
michael@0 549 // or any other sort of timezone information. the best it does is
michael@0 550 // give us a timezone offset that we can use to convert the local
michael@0 551 // time to UTC. furthermore, it has no representation for floating
michael@0 552 // date-times. therefore, all dates are converted to UTC.
michael@0 553
michael@0 554 dt.setUtc(true);
michael@0 555
michael@0 556 return dt;
michael@0 557 }
michael@0 558 }

mercurial