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

changeset 0
fb9019fb1bf7
child 4
45d57ecba757
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/net/fortuna/ical4j/data/HCalendarParser.java	Tue Feb 10 18:12:00 2015 +0100
     1.3 @@ -0,0 +1,558 @@
     1.4 +/**
     1.5 + * Copyright (c) 2012, Ben Fortuna
     1.6 + * All rights reserved.
     1.7 + *
     1.8 + * Redistribution and use in source and binary forms, with or without
     1.9 + * modification, are permitted provided that the following conditions
    1.10 + * are met:
    1.11 + *
    1.12 + *  o Redistributions of source code must retain the above copyright
    1.13 + * notice, this list of conditions and the following disclaimer.
    1.14 + *
    1.15 + *  o Redistributions in binary form must reproduce the above copyright
    1.16 + * notice, this list of conditions and the following disclaimer in the
    1.17 + * documentation and/or other materials provided with the distribution.
    1.18 + *
    1.19 + *  o Neither the name of Ben Fortuna nor the names of any other contributors
    1.20 + * may be used to endorse or promote products derived from this software
    1.21 + * without specific prior written permission.
    1.22 + *
    1.23 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    1.24 + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    1.25 + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    1.26 + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    1.27 + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    1.28 + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    1.29 + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    1.30 + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    1.31 + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    1.32 + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    1.33 + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    1.34 + */
    1.35 +package net.fortuna.ical4j.data;
    1.36 +
    1.37 +import java.io.IOException;
    1.38 +import java.io.InputStream;
    1.39 +import java.io.Reader;
    1.40 +import java.net.URISyntaxException;
    1.41 +import java.text.ParseException;
    1.42 +import java.text.SimpleDateFormat;
    1.43 +import java.util.ArrayList;
    1.44 +import java.util.Iterator;
    1.45 +import java.util.List;
    1.46 +
    1.47 +import javax.xml.XMLConstants;
    1.48 +import javax.xml.parsers.DocumentBuilderFactory;
    1.49 +import javax.xml.parsers.ParserConfigurationException;
    1.50 +import javax.xml.xpath.XPath;
    1.51 +import javax.xml.xpath.XPathConstants;
    1.52 +import javax.xml.xpath.XPathException;
    1.53 +import javax.xml.xpath.XPathExpression;
    1.54 +import javax.xml.xpath.XPathFactory;
    1.55 +
    1.56 +import net.fortuna.ical4j.model.CalendarException;
    1.57 +import net.fortuna.ical4j.model.Component;
    1.58 +import net.fortuna.ical4j.model.Date;
    1.59 +import net.fortuna.ical4j.model.DateTime;
    1.60 +import net.fortuna.ical4j.model.Parameter;
    1.61 +import net.fortuna.ical4j.model.Property;
    1.62 +import net.fortuna.ical4j.model.parameter.Value;
    1.63 +import net.fortuna.ical4j.model.property.Version;
    1.64 +
    1.65 +import org.apache.commons.lang.StringUtils;
    1.66 +import org.apache.commons.logging.Log;
    1.67 +import org.apache.commons.logging.LogFactory;
    1.68 +import org.w3c.dom.DOMException;
    1.69 +import org.w3c.dom.Document;
    1.70 +import org.w3c.dom.Element;
    1.71 +import org.w3c.dom.Node;
    1.72 +import org.w3c.dom.NodeList;
    1.73 +import org.xml.sax.InputSource;
    1.74 +import org.xml.sax.SAXException;
    1.75 +import org.xml.sax.SAXParseException;
    1.76 +
    1.77 +/**
    1.78 + * A {@link CalendarParser} that parses XHTML documents that include calendar data marked up with the hCalendar
    1.79 + * microformat.
    1.80 + * <p>
    1.81 + * The parser treats the entire document as a single "vcalendar" context, ignoring any <code>vcalendar</code> elements
    1.82 + * and adding all components in the document to a single generated calendar.
    1.83 + * </p>
    1.84 + * <p>
    1.85 + * Since hCalendar does not include product information, the <code>PRODID</code> property is omitted from the generated
    1.86 + * calendar. The hCalendar profile is supposed to define the iCalendar version that it represents, but it does not, so
    1.87 + * version 2.0 is assumed.
    1.88 + * </p>
    1.89 + * <h3>Supported Components</h3>
    1.90 + * <p>
    1.91 + * This parser recognizes only "vevent" components.
    1.92 + * </p>
    1.93 + * <h3>Supported Properties</h3>
    1.94 + * <p>
    1.95 + * This parser recognizes the following properties:
    1.96 + * </p>
    1.97 + * <ul>
    1.98 + * <li>"dtstart"</li>
    1.99 + * <li>"dtend"</li>
   1.100 + * <li>"duration"</li>
   1.101 + * <li>"summary"</li>
   1.102 + * <li>"uid"</li>
   1.103 + * <li>"dtstamp"</li>
   1.104 + * <li>"category"</li>
   1.105 + * <li>"location"</li>
   1.106 + * <li>"url"</li>
   1.107 + * <li>"description"</li>
   1.108 + * <li>"last-modified"</li>
   1.109 + * <li>"status"</li>
   1.110 + * <li>"class"</li>
   1.111 + * <li>"attendee"</li>
   1.112 + * <li>"contact"</li>
   1.113 + * <li>"organizer"</li>
   1.114 + * </ul>
   1.115 + * <p>
   1.116 + * hCalendar allows for some properties to be represented by nested microformat records, including hCard, adr and geo.
   1.117 + * This parser does not recognize these records. It simply accumulates the text content of any child elements of the
   1.118 + * property element and uses the resulting string as the property value.
   1.119 + * </p>
   1.120 + * <h4>Date and Date-Time Properties</h4>
   1.121 + * <p>
   1.122 + * hCalendar date-time values are formatted according to RFC 3339. There is no representation in this specification for
   1.123 + * time zone ids. All date-times are specified either in UTC or with an offset that can be used to convert the local
   1.124 + * time into UTC. Neither does hCal provide a reprsentation for floating date-times. Therefore, all date-time values
   1.125 + * produced by this parser are in UTC.
   1.126 + * </p>
   1.127 + * <p>
   1.128 + * Some examples in the wild provide date and date-time values in iCalendar format rather than RFC 3339 format. Although
   1.129 + * not technically legal according to spec, these values are accepted. In this case, floating date-times are produced by
   1.130 + * the parser.
   1.131 + * </p>
   1.132 + * <h3>Supported Parameters</h3>
   1.133 + * <p>
   1.134 + * hCalendar does not define attributes, nested elements or other information elements representing parameter data.
   1.135 + * Therefore, this parser does not set any property parameters except as implied by property value data (e.g.
   1.136 + * VALUE=DATE-TIME or VALUE=DATE for date-time properties).
   1.137 + * </p>
   1.138 + */
   1.139 +public class HCalendarParser implements CalendarParser {
   1.140 +    
   1.141 +    private static final Log LOG = LogFactory.getLog(HCalendarParser.class);
   1.142 +    
   1.143 +    private static final DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
   1.144 +    private static final XPath XPATH = XPathFactory.newInstance().newXPath();
   1.145 +    private static final XPathExpression XPATH_METHOD;
   1.146 +    private static final XPathExpression XPATH_VEVENTS;
   1.147 +    private static final XPathExpression XPATH_DTSTART;
   1.148 +    private static final XPathExpression XPATH_DTEND;
   1.149 +    private static final XPathExpression XPATH_DURATION;
   1.150 +    private static final XPathExpression XPATH_SUMMARY;
   1.151 +    private static final XPathExpression XPATH_UID;
   1.152 +    private static final XPathExpression XPATH_DTSTAMP;
   1.153 +    private static final XPathExpression XPATH_CATEGORY;
   1.154 +    private static final XPathExpression XPATH_LOCATION;
   1.155 +    private static final XPathExpression XPATH_URL;
   1.156 +    private static final XPathExpression XPATH_DESCRIPTION;
   1.157 +    private static final XPathExpression XPATH_LAST_MODIFIED;
   1.158 +    private static final XPathExpression XPATH_STATUS;
   1.159 +    private static final XPathExpression XPATH_CLASS;
   1.160 +    private static final XPathExpression XPATH_ATTENDEE;
   1.161 +    private static final XPathExpression XPATH_CONTACT;
   1.162 +    private static final XPathExpression XPATH_ORGANIZER;
   1.163 +    private static final XPathExpression XPATH_SEQUENCE;
   1.164 +    private static final XPathExpression XPATH_ATTACH;
   1.165 +    private static final String HCAL_DATE_PATTERN = "yyyy-MM-dd";
   1.166 +    private static final SimpleDateFormat HCAL_DATE_FORMAT = new SimpleDateFormat(HCAL_DATE_PATTERN);
   1.167 +    private static final String HCAL_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssz";
   1.168 +    private static final SimpleDateFormat HCAL_DATE_TIME_FORMAT = new SimpleDateFormat(HCAL_DATE_TIME_PATTERN);
   1.169 +
   1.170 +    static {
   1.171 +        BUILDER_FACTORY.setNamespaceAware(true);
   1.172 +        BUILDER_FACTORY.setIgnoringComments(true);
   1.173 +
   1.174 +        XPATH_METHOD = compileExpression("//*[contains(@class, 'method')]");
   1.175 +        XPATH_VEVENTS = compileExpression("//*[contains(@class, 'vevent')]");
   1.176 +        XPATH_DTSTART = compileExpression(".//*[contains(@class, 'dtstart')]");
   1.177 +        XPATH_DTEND = compileExpression(".//*[contains(@class, 'dtend')]");
   1.178 +        XPATH_DURATION = compileExpression(".//*[contains(@class, 'duration')]");
   1.179 +        XPATH_SUMMARY = compileExpression(".//*[contains(@class, 'summary')]");
   1.180 +        XPATH_UID = compileExpression(".//*[contains(@class, 'uid')]");
   1.181 +        XPATH_DTSTAMP = compileExpression(".//*[contains(@class, 'dtstamp')]");
   1.182 +        XPATH_CATEGORY = compileExpression(".//*[contains(@class, 'category')]");
   1.183 +        XPATH_LOCATION = compileExpression(".//*[contains(@class, 'location')]");
   1.184 +        XPATH_URL = compileExpression(".//*[contains(@class, 'url')]");
   1.185 +        XPATH_DESCRIPTION = compileExpression(".//*[contains(@class, 'description')]");
   1.186 +        XPATH_LAST_MODIFIED = compileExpression(".//*[contains(@class, 'last-modified')]");
   1.187 +        XPATH_STATUS = compileExpression(".//*[contains(@class, 'status')]");
   1.188 +        XPATH_CLASS = compileExpression(".//*[contains(@class, 'class')]");
   1.189 +        XPATH_ATTENDEE = compileExpression(".//*[contains(@class, 'attendee')]");
   1.190 +        XPATH_CONTACT = compileExpression(".//*[contains(@class, 'contact')]");
   1.191 +        XPATH_ORGANIZER = compileExpression(".//*[contains(@class, 'organizer')]");
   1.192 +        XPATH_SEQUENCE = compileExpression(".//*[contains(@class, 'sequence')]");
   1.193 +        XPATH_ATTACH = compileExpression(".//*[contains(@class, 'attach')]");
   1.194 +    }
   1.195 +
   1.196 +    private static XPathExpression compileExpression(String expr) {
   1.197 +        try {
   1.198 +            return XPATH.compile(expr);
   1.199 +        } catch (XPathException e) {
   1.200 +            throw new CalendarException(e);
   1.201 +        }
   1.202 +    }
   1.203 +
   1.204 +    /**
   1.205 +     * {@inheritDoc}
   1.206 +     */
   1.207 +    public void parse(InputStream in, ContentHandler handler) throws IOException, ParserException {
   1.208 +        parse(new InputSource(in), handler);
   1.209 +    }
   1.210 +
   1.211 +    /**
   1.212 +     * {@inheritDoc}
   1.213 +     */
   1.214 +    public void parse(Reader in, ContentHandler handler) throws IOException, ParserException {
   1.215 +        parse(new InputSource(in), handler);
   1.216 +    }
   1.217 +
   1.218 +    private void parse(InputSource in, ContentHandler handler) throws IOException, ParserException {
   1.219 +        try {
   1.220 +            Document d = BUILDER_FACTORY.newDocumentBuilder().parse(in);
   1.221 +            buildCalendar(d, handler);
   1.222 +        } catch (ParserConfigurationException e) {
   1.223 +            throw new CalendarException(e);
   1.224 +        } catch (SAXException e) {
   1.225 +            if (e instanceof SAXParseException) {
   1.226 +                SAXParseException pe = (SAXParseException) e;
   1.227 +                throw new ParserException("Could not parse XML", pe.getLineNumber(), e);
   1.228 +            }
   1.229 +            throw new ParserException(e.getMessage(), -1, e);
   1.230 +        }
   1.231 +    }
   1.232 +
   1.233 +    private static NodeList findNodes(XPathExpression expr, Object context) throws ParserException {
   1.234 +        try {
   1.235 +            return (NodeList) expr.evaluate(context, XPathConstants.NODESET);
   1.236 +        } catch (XPathException e) {
   1.237 +            throw new ParserException("Unable to find nodes", -1, e);
   1.238 +        }
   1.239 +    }
   1.240 +
   1.241 +    private static Node findNode(XPathExpression expr, Object context) throws ParserException {
   1.242 +        try {
   1.243 +            return (Node) expr.evaluate(context, XPathConstants.NODE);
   1.244 +        } catch (XPathException e) {
   1.245 +            throw new ParserException("Unable to find node", -1, e);
   1.246 +        }
   1.247 +    }
   1.248 +
   1.249 +    private static List findElements(XPathExpression expr, Object context) throws ParserException {
   1.250 +        NodeList nodes = findNodes(expr, context);
   1.251 +        ArrayList elements = new ArrayList();
   1.252 +        for (int i = 0; i < nodes.getLength(); i++) {
   1.253 +            Node n = nodes.item(i);
   1.254 +            if (n instanceof Element)
   1.255 +                elements.add((Element) n);
   1.256 +        }
   1.257 +        return elements;
   1.258 +    }
   1.259 +
   1.260 +    private static Element findElement(XPathExpression expr, Object context) throws ParserException {
   1.261 +        Node n = findNode(expr, context);
   1.262 +        if (n == null || (!(n instanceof Element)))
   1.263 +            return null;
   1.264 +        return (Element) n;
   1.265 +    }
   1.266 +
   1.267 +    private static String getTextContent(Element element) throws ParserException {
   1.268 +        try {
   1.269 +            String content = element.getFirstChild().getNodeValue();
   1.270 +            if (content != null) {
   1.271 +                return content.trim().replaceAll("\\s+", " ");
   1.272 +            }
   1.273 +            return content;
   1.274 +        } catch (DOMException e) {
   1.275 +            throw new ParserException("Unable to get text content for element " + element.getNodeName(), -1, e);
   1.276 +        }
   1.277 +    }
   1.278 +
   1.279 +    private void buildCalendar(Document d, ContentHandler handler) throws ParserException {
   1.280 +        // "The root class name for hCalendar is "vcalendar". An element with a
   1.281 +        // class name of "vcalendar" is itself called an hCalendar.
   1.282 +        //
   1.283 +        // The root class name for events is "vevent". An element with a class
   1.284 +        // name of "vevent" is itself called an hCalender event.
   1.285 +        //
   1.286 +        // For authoring convenience, both "vevent" and "vcalendar" are
   1.287 +        // treated as root class names for parsing purposes. If a document
   1.288 +        // contains elements with class name "vevent" but not "vcalendar", the
   1.289 +        // entire document has an implied "vcalendar" context."
   1.290 +
   1.291 +        // XXX: We assume that the entire document has a single vcalendar
   1.292 +        // context. It is possible that the document contains more than one
   1.293 +        // vcalendar element. In this case, we should probably only process
   1.294 +        // that element and log a warning about skipping the others.
   1.295 +
   1.296 +        if (LOG.isDebugEnabled())
   1.297 +            LOG.debug("Building calendar");
   1.298 +
   1.299 +        handler.startCalendar();
   1.300 +
   1.301 +        // no PRODID, as the using application should set that itself
   1.302 +
   1.303 +        handler.startProperty(Property.VERSION);
   1.304 +        try {
   1.305 +            handler.propertyValue(Version.VERSION_2_0.getValue());
   1.306 +        } catch (Exception e) {
   1.307 +        }
   1.308 +        ;
   1.309 +        handler.endProperty(Property.VERSION);
   1.310 +
   1.311 +        Element method = findElement(XPATH_METHOD, d);
   1.312 +        if (method != null) {
   1.313 +            buildProperty(method, Property.METHOD, handler);
   1.314 +        }
   1.315 +
   1.316 +        List vevents = findElements(XPATH_VEVENTS, d);
   1.317 +        for (Iterator i = vevents.iterator(); i.hasNext();) {
   1.318 +            Element vevent = (Element) i.next();
   1.319 +            buildEvent(vevent, handler);
   1.320 +        }
   1.321 +
   1.322 +        // XXX: support other "first class components": vjournal, vtodo,
   1.323 +        // vfreebusy, vavailability, vvenue
   1.324 +
   1.325 +        handler.endCalendar();
   1.326 +    }
   1.327 +
   1.328 +    private void buildEvent(Element element, ContentHandler handler) throws ParserException {
   1.329 +        if (LOG.isDebugEnabled())
   1.330 +            LOG.debug("Building event");
   1.331 +
   1.332 +        handler.startComponent(Component.VEVENT);
   1.333 +
   1.334 +        buildProperty(findElement(XPATH_DTSTART, element), Property.DTSTART, handler);
   1.335 +        buildProperty(findElement(XPATH_DTEND, element), Property.DTEND, handler);
   1.336 +        buildProperty(findElement(XPATH_DURATION, element), Property.DURATION, handler);
   1.337 +        buildProperty(findElement(XPATH_SUMMARY, element), Property.SUMMARY, handler);
   1.338 +        buildProperty(findElement(XPATH_UID, element), Property.UID, handler);
   1.339 +        buildProperty(findElement(XPATH_DTSTAMP, element), Property.DTSTAMP, handler);
   1.340 +        List categories = findElements(XPATH_CATEGORY, element);
   1.341 +        for (Iterator i = categories.iterator(); i.hasNext();) {
   1.342 +            Element category = (Element) i.next();
   1.343 +            buildProperty(category, Property.CATEGORIES, handler);
   1.344 +        }
   1.345 +        buildProperty(findElement(XPATH_LOCATION, element), Property.LOCATION, handler);
   1.346 +        buildProperty(findElement(XPATH_URL, element), Property.URL, handler);
   1.347 +        buildProperty(findElement(XPATH_DESCRIPTION, element), Property.DESCRIPTION, handler);
   1.348 +        buildProperty(findElement(XPATH_LAST_MODIFIED, element), Property.LAST_MODIFIED, handler);
   1.349 +        buildProperty(findElement(XPATH_STATUS, element), Property.STATUS, handler);
   1.350 +        buildProperty(findElement(XPATH_CLASS, element), Property.CLASS, handler);
   1.351 +        List attendees = findElements(XPATH_ATTENDEE, element);
   1.352 +        for (Iterator i = attendees.iterator(); i.hasNext();) {
   1.353 +            Element attendee = (Element) i.next();
   1.354 +            buildProperty(attendee, Property.ATTENDEE, handler);
   1.355 +        }
   1.356 +        buildProperty(findElement(XPATH_CONTACT, element), Property.CONTACT, handler);
   1.357 +        buildProperty(findElement(XPATH_ORGANIZER, element), Property.ORGANIZER, handler);
   1.358 +        buildProperty(findElement(XPATH_SEQUENCE, element), Property.SEQUENCE, handler);
   1.359 +        buildProperty(findElement(XPATH_ATTACH, element), Property.ATTACH, handler);
   1.360 +
   1.361 +        handler.endComponent(Component.VEVENT);
   1.362 +    }
   1.363 +
   1.364 +    private void buildProperty(Element element, String propName, ContentHandler handler) throws ParserException {
   1.365 +        if (element == null)
   1.366 +            return;
   1.367 +
   1.368 +        if (LOG.isDebugEnabled())
   1.369 +            LOG.debug("Building property " + propName);
   1.370 +
   1.371 +        String className = className(propName);
   1.372 +        String elementName = element.getLocalName().toLowerCase();
   1.373 +
   1.374 +        String value = null;
   1.375 +        if (elementName.equals("abbr")) {
   1.376 +            // "If an <abbr> element is used for a property, then the 'title'
   1.377 +            // attribute of the <abbr> element is the value of the property,
   1.378 +            // instead of the contents of the element, which instead provide a
   1.379 +            // human presentable version of the value."
   1.380 +            value = element.getAttribute("title");
   1.381 +            if (StringUtils.isBlank(value))
   1.382 +                throw new ParserException("Abbr element '" + className + "' requires a non-empty title", -1);
   1.383 +            if (LOG.isDebugEnabled())
   1.384 +                LOG.debug("Setting value '" + value + "' from title attribute");
   1.385 +        } else if (isHeaderElement(elementName)) {
   1.386 +            // try title first. if that's not set, fall back to text content.
   1.387 +            value = element.getAttribute("title");
   1.388 +            if (!StringUtils.isBlank(value)) {
   1.389 +                if (LOG.isDebugEnabled())
   1.390 +                    LOG.debug("Setting value '" + value + "' from title attribute");
   1.391 +            } else {
   1.392 +                value = getTextContent(element);
   1.393 +                if (LOG.isDebugEnabled())
   1.394 +                    LOG.debug("Setting value '" + value + "' from text content");
   1.395 +            }
   1.396 +        } else if (elementName.equals("a") && isUrlProperty(propName)) {
   1.397 +            value = element.getAttribute("href");
   1.398 +            if (StringUtils.isBlank(value))
   1.399 +                throw new ParserException("A element '" + className + "' requires a non-empty href", -1);
   1.400 +            if (LOG.isDebugEnabled())
   1.401 +                LOG.debug("Setting value '" + value + "' from href attribute");
   1.402 +        } else if (elementName.equals("img")) {
   1.403 +            if (isUrlProperty(propName)) {
   1.404 +                value = element.getAttribute("src");
   1.405 +                if (StringUtils.isBlank(value))
   1.406 +                    throw new ParserException("Img element '" + className + "' requires a non-empty src", -1);
   1.407 +                if (LOG.isDebugEnabled())
   1.408 +                    LOG.debug("Setting value '" + value + "' from src attribute");
   1.409 +            } else {
   1.410 +                value = element.getAttribute("alt");
   1.411 +                if (StringUtils.isBlank(value))
   1.412 +                    throw new ParserException("Img element '" + className + "' requires a non-empty alt", -1);
   1.413 +                if (LOG.isDebugEnabled())
   1.414 +                    LOG.debug("Setting value '" + value + "' from alt attribute");
   1.415 +            }
   1.416 +        } else {
   1.417 +            value = getTextContent(element);
   1.418 +            if (!StringUtils.isBlank(value)) {
   1.419 +                if (LOG.isDebugEnabled())
   1.420 +                    LOG.debug("Setting value '" + value + "' from text content");
   1.421 +            }
   1.422 +        }
   1.423 +
   1.424 +        if (StringUtils.isBlank(value)) {
   1.425 +            if (LOG.isDebugEnabled())
   1.426 +                LOG.debug("Skipping property with empty value");
   1.427 +            return;
   1.428 +        }
   1.429 +
   1.430 +        handler.startProperty(propName);
   1.431 +
   1.432 +        // if it's a date property, we have to convert from the
   1.433 +        // hCalendar-formatted date (RFC 3339) to an iCalendar-formatted date
   1.434 +        if (isDateProperty(propName)) {
   1.435 +            try {
   1.436 +                Date date = icalDate(value);
   1.437 +                value = date.toString();
   1.438 +
   1.439 +                if (!(date instanceof DateTime))
   1.440 +                    try {
   1.441 +                        handler.parameter(Parameter.VALUE, Value.DATE.getValue());
   1.442 +                    } catch (Exception e) {
   1.443 +                    }
   1.444 +            } catch (ParseException e) {
   1.445 +                throw new ParserException("Malformed date value for element '" + className + "'", -1, e);
   1.446 +            }
   1.447 +        }
   1.448 +
   1.449 +        if (isTextProperty(propName)) {
   1.450 +            String lang = element.getAttributeNS(XMLConstants.XML_NS_URI, "lang");
   1.451 +            if (!StringUtils.isBlank(lang))
   1.452 +                try {
   1.453 +                    handler.parameter(Parameter.LANGUAGE, lang);
   1.454 +                } catch (Exception e) {
   1.455 +                }
   1.456 +        }
   1.457 +
   1.458 +        // XXX: other parameters?
   1.459 +
   1.460 +        try {
   1.461 +            handler.propertyValue(value);
   1.462 +        } catch (URISyntaxException e) {
   1.463 +            throw new ParserException("Malformed URI value for element '" + className + "'", -1, e);
   1.464 +        } catch (ParseException e) {
   1.465 +            throw new ParserException("Malformed value for element '" + className + "'", -1, e);
   1.466 +        } catch (IOException e) {
   1.467 +            throw new CalendarException(e);
   1.468 +        }
   1.469 +
   1.470 +        handler.endProperty(propName);
   1.471 +    }
   1.472 +
   1.473 +    // "The basic format of hCalendar is to use iCalendar object/property
   1.474 +    // names in lower-case for class names ..."
   1.475 +    /*
   1.476 +     * private static String _icalName(Element element) { return element.getAttribute("class").toUpperCase(); }
   1.477 +     */
   1.478 +
   1.479 +    private static String className(String propName) {
   1.480 +        return propName.toLowerCase();
   1.481 +    }
   1.482 +
   1.483 +    private static boolean isHeaderElement(String name) {
   1.484 +        return (name.equals("h1") || name.equals("h2") || name.equals("h3")
   1.485 +                || name.equals("h4") || name.equals("h5") || name
   1.486 +                .equals("h6"));
   1.487 +    }
   1.488 +
   1.489 +    private static boolean isDateProperty(String name) {
   1.490 +        return (name.equals(Property.DTSTART) || name.equals(Property.DTEND) || name.equals(Property.DTSTAMP) || name
   1.491 +                .equals(Property.LAST_MODIFIED));
   1.492 +    }
   1.493 +
   1.494 +    private static boolean isUrlProperty(String name) {
   1.495 +        return (name.equals(Property.URL));
   1.496 +    }
   1.497 +
   1.498 +    private static boolean isTextProperty(String name) {
   1.499 +        return (name.equals(Property.SUMMARY) || name.equals(Property.LOCATION) || name.equals(Property.CATEGORIES)
   1.500 +                || name.equals(Property.DESCRIPTION) || name.equals(Property.ATTENDEE)
   1.501 +                || name.equals(Property.CONTACT) || name
   1.502 +                .equals(Property.ORGANIZER));
   1.503 +    }
   1.504 +
   1.505 +    private static Date icalDate(String original) throws ParseException {
   1.506 +        // in the real world, some generators use iCalendar formatted
   1.507 +        // dates and date-times, so try parsing those formats first before
   1.508 +        // going to RFC 3339 formats
   1.509 +
   1.510 +        if (original.indexOf('T') == -1) {
   1.511 +            // date-only
   1.512 +            try {
   1.513 +                // for some reason Date's pattern matches yyyy-MM-dd, so
   1.514 +                // don't check it if we find -
   1.515 +                if (original.indexOf('-') == -1)
   1.516 +                    return new Date(original);
   1.517 +            } catch (Exception e) {
   1.518 +            }
   1.519 +            return new Date(HCAL_DATE_FORMAT.parse(original));
   1.520 +        }
   1.521 +
   1.522 +        try {
   1.523 +            return new DateTime(original);
   1.524 +        } catch (Exception e) {
   1.525 +        }
   1.526 +
   1.527 +        // the date-time value can represent its time zone in a few different
   1.528 +        // ways. we have to normalize those to match our pattern.
   1.529 +
   1.530 +        String normalized = null;
   1.531 +
   1.532 +        if (LOG.isDebugEnabled())
   1.533 +            LOG.debug("normalizing date-time " + original);
   1.534 +
   1.535 +        // 2002-10-09T19:00:00Z
   1.536 +        if (original.charAt(original.length() - 1) == 'Z') {
   1.537 +            normalized = original.replaceAll("Z", "GMT-00:00");
   1.538 +        }
   1.539 +        // 2002-10-10T00:00:00+05:00
   1.540 +        else if (original.indexOf("GMT") == -1
   1.541 +                && (original.charAt(original.length() - 6) == '+' || original.charAt(original.length() - 6) == '-')) {
   1.542 +            String tzId = "GMT" + original.substring(original.length() - 6);
   1.543 +            normalized = original.substring(0, original.length() - 6) + tzId;
   1.544 +        } else {
   1.545 +            // 2002-10-10T00:00:00GMT+05:00
   1.546 +            normalized = original;
   1.547 +        }
   1.548 +
   1.549 +        DateTime dt = new DateTime(HCAL_DATE_TIME_FORMAT.parse(normalized));
   1.550 +
   1.551 +        // hCalendar does not specify a representation for timezone ids
   1.552 +        // or any other sort of timezone information. the best it does is
   1.553 +        // give us a timezone offset that we can use to convert the local
   1.554 +        // time to UTC. furthermore, it has no representation for floating
   1.555 +        // date-times. therefore, all dates are converted to UTC.
   1.556 +
   1.557 +        dt.setUtc(true);
   1.558 +
   1.559 +        return dt;
   1.560 +    }
   1.561 +}

mercurial