Tue, 10 Feb 2015 19:58:00 +0100
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 | } |