michael@0: /** michael@0: * Copyright (c) 2012, Ben Fortuna michael@0: * All rights reserved. michael@0: * michael@0: * Redistribution and use in source and binary forms, with or without michael@0: * modification, are permitted provided that the following conditions michael@0: * are met: michael@0: * michael@0: * o Redistributions of source code must retain the above copyright michael@0: * notice, this list of conditions and the following disclaimer. michael@0: * michael@0: * o Redistributions in binary form must reproduce the above copyright michael@0: * notice, this list of conditions and the following disclaimer in the michael@0: * documentation and/or other materials provided with the distribution. michael@0: * michael@0: * o Neither the name of Ben Fortuna nor the names of any other contributors michael@0: * may be used to endorse or promote products derived from this software michael@0: * without specific prior written permission. michael@0: * michael@0: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS michael@0: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT michael@0: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR michael@0: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR michael@0: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, michael@0: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, michael@0: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR michael@0: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF michael@0: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING michael@0: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS michael@0: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: */ michael@0: package net.fortuna.ical4j.data; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.io.Reader; michael@0: import java.net.URISyntaxException; michael@0: import java.text.ParseException; michael@0: import java.text.SimpleDateFormat; michael@0: import java.util.ArrayList; michael@0: import java.util.Iterator; michael@0: import java.util.List; michael@0: michael@0: import javax.xml.XMLConstants; michael@0: import javax.xml.parsers.DocumentBuilderFactory; michael@0: import javax.xml.parsers.ParserConfigurationException; michael@0: import javax.xml.xpath.XPath; michael@0: import javax.xml.xpath.XPathConstants; michael@0: import javax.xml.xpath.XPathException; michael@0: import javax.xml.xpath.XPathExpression; michael@0: import javax.xml.xpath.XPathFactory; michael@0: michael@0: import net.fortuna.ical4j.model.CalendarException; michael@0: import net.fortuna.ical4j.model.Component; michael@0: import net.fortuna.ical4j.model.Date; michael@0: import net.fortuna.ical4j.model.DateTime; michael@0: import net.fortuna.ical4j.model.Parameter; michael@0: import net.fortuna.ical4j.model.Property; michael@0: import net.fortuna.ical4j.model.parameter.Value; michael@0: import net.fortuna.ical4j.model.property.Version; michael@0: michael@0: import org.apache.commons.lang.StringUtils; michael@0: import org.apache.commons.logging.Log; michael@0: import org.apache.commons.logging.LogFactory; michael@0: import org.w3c.dom.DOMException; michael@0: import org.w3c.dom.Document; michael@0: import org.w3c.dom.Element; michael@0: import org.w3c.dom.Node; michael@0: import org.w3c.dom.NodeList; michael@0: import org.xml.sax.InputSource; michael@0: import org.xml.sax.SAXException; michael@0: import org.xml.sax.SAXParseException; michael@0: michael@0: /** michael@0: * A {@link CalendarParser} that parses XHTML documents that include calendar data marked up with the hCalendar michael@0: * microformat. michael@0: *
michael@0: * The parser treats the entire document as a single "vcalendar" context, ignoring any vcalendar
elements
michael@0: * and adding all components in the document to a single generated calendar.
michael@0: *
michael@0: * Since hCalendar does not include product information, the PRODID
property is omitted from the generated
michael@0: * calendar. The hCalendar profile is supposed to define the iCalendar version that it represents, but it does not, so
michael@0: * version 2.0 is assumed.
michael@0: *
michael@0: * This parser recognizes only "vevent" components. michael@0: *
michael@0: *michael@0: * This parser recognizes the following properties: michael@0: *
michael@0: *michael@0: * hCalendar allows for some properties to be represented by nested microformat records, including hCard, adr and geo. michael@0: * This parser does not recognize these records. It simply accumulates the text content of any child elements of the michael@0: * property element and uses the resulting string as the property value. michael@0: *
michael@0: *michael@0: * hCalendar date-time values are formatted according to RFC 3339. There is no representation in this specification for michael@0: * 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: * time into UTC. Neither does hCal provide a reprsentation for floating date-times. Therefore, all date-time values michael@0: * produced by this parser are in UTC. michael@0: *
michael@0: *michael@0: * Some examples in the wild provide date and date-time values in iCalendar format rather than RFC 3339 format. Although michael@0: * not technically legal according to spec, these values are accepted. In this case, floating date-times are produced by michael@0: * the parser. michael@0: *
michael@0: *michael@0: * hCalendar does not define attributes, nested elements or other information elements representing parameter data. michael@0: * Therefore, this parser does not set any property parameters except as implied by property value data (e.g. michael@0: * VALUE=DATE-TIME or VALUE=DATE for date-time properties). michael@0: *
michael@0: */ michael@0: public class HCalendarParser implements CalendarParser { michael@0: michael@0: private static final Log LOG = LogFactory.getLog(HCalendarParser.class); michael@0: michael@0: private static final DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); michael@0: private static final XPath XPATH = XPathFactory.newInstance().newXPath(); michael@0: private static final XPathExpression XPATH_METHOD; michael@0: private static final XPathExpression XPATH_VEVENTS; michael@0: private static final XPathExpression XPATH_DTSTART; michael@0: private static final XPathExpression XPATH_DTEND; michael@0: private static final XPathExpression XPATH_DURATION; michael@0: private static final XPathExpression XPATH_SUMMARY; michael@0: private static final XPathExpression XPATH_UID; michael@0: private static final XPathExpression XPATH_DTSTAMP; michael@0: private static final XPathExpression XPATH_CATEGORY; michael@0: private static final XPathExpression XPATH_LOCATION; michael@0: private static final XPathExpression XPATH_URL; michael@0: private static final XPathExpression XPATH_DESCRIPTION; michael@0: private static final XPathExpression XPATH_LAST_MODIFIED; michael@0: private static final XPathExpression XPATH_STATUS; michael@0: private static final XPathExpression XPATH_CLASS; michael@0: private static final XPathExpression XPATH_ATTENDEE; michael@0: private static final XPathExpression XPATH_CONTACT; michael@0: private static final XPathExpression XPATH_ORGANIZER; michael@0: private static final XPathExpression XPATH_SEQUENCE; michael@0: private static final XPathExpression XPATH_ATTACH; michael@0: private static final String HCAL_DATE_PATTERN = "yyyy-MM-dd"; michael@0: private static final SimpleDateFormat HCAL_DATE_FORMAT = new SimpleDateFormat(HCAL_DATE_PATTERN); michael@0: private static final String HCAL_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ssz"; michael@0: private static final SimpleDateFormat HCAL_DATE_TIME_FORMAT = new SimpleDateFormat(HCAL_DATE_TIME_PATTERN); michael@0: michael@0: static { michael@0: BUILDER_FACTORY.setNamespaceAware(true); michael@0: BUILDER_FACTORY.setIgnoringComments(true); michael@0: michael@0: XPATH_METHOD = compileExpression("//*[contains(@class, 'method')]"); michael@0: XPATH_VEVENTS = compileExpression("//*[contains(@class, 'vevent')]"); michael@0: XPATH_DTSTART = compileExpression(".//*[contains(@class, 'dtstart')]"); michael@0: XPATH_DTEND = compileExpression(".//*[contains(@class, 'dtend')]"); michael@0: XPATH_DURATION = compileExpression(".//*[contains(@class, 'duration')]"); michael@0: XPATH_SUMMARY = compileExpression(".//*[contains(@class, 'summary')]"); michael@0: XPATH_UID = compileExpression(".//*[contains(@class, 'uid')]"); michael@0: XPATH_DTSTAMP = compileExpression(".//*[contains(@class, 'dtstamp')]"); michael@0: XPATH_CATEGORY = compileExpression(".//*[contains(@class, 'category')]"); michael@0: XPATH_LOCATION = compileExpression(".//*[contains(@class, 'location')]"); michael@0: XPATH_URL = compileExpression(".//*[contains(@class, 'url')]"); michael@0: XPATH_DESCRIPTION = compileExpression(".//*[contains(@class, 'description')]"); michael@0: XPATH_LAST_MODIFIED = compileExpression(".//*[contains(@class, 'last-modified')]"); michael@0: XPATH_STATUS = compileExpression(".//*[contains(@class, 'status')]"); michael@0: XPATH_CLASS = compileExpression(".//*[contains(@class, 'class')]"); michael@0: XPATH_ATTENDEE = compileExpression(".//*[contains(@class, 'attendee')]"); michael@0: XPATH_CONTACT = compileExpression(".//*[contains(@class, 'contact')]"); michael@0: XPATH_ORGANIZER = compileExpression(".//*[contains(@class, 'organizer')]"); michael@0: XPATH_SEQUENCE = compileExpression(".//*[contains(@class, 'sequence')]"); michael@0: XPATH_ATTACH = compileExpression(".//*[contains(@class, 'attach')]"); michael@0: } michael@0: michael@0: private static XPathExpression compileExpression(String expr) { michael@0: try { michael@0: return XPATH.compile(expr); michael@0: } catch (XPathException e) { michael@0: throw new CalendarException(e); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public void parse(InputStream in, ContentHandler handler) throws IOException, ParserException { michael@0: parse(new InputSource(in), handler); michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: public void parse(Reader in, ContentHandler handler) throws IOException, ParserException { michael@0: parse(new InputSource(in), handler); michael@0: } michael@0: michael@0: private void parse(InputSource in, ContentHandler handler) throws IOException, ParserException { michael@0: try { michael@0: Document d = BUILDER_FACTORY.newDocumentBuilder().parse(in); michael@0: buildCalendar(d, handler); michael@0: } catch (ParserConfigurationException e) { michael@0: throw new CalendarException(e); michael@0: } catch (SAXException e) { michael@0: if (e instanceof SAXParseException) { michael@0: SAXParseException pe = (SAXParseException) e; michael@0: throw new ParserException("Could not parse XML", pe.getLineNumber(), e); michael@0: } michael@0: throw new ParserException(e.getMessage(), -1, e); michael@0: } michael@0: } michael@0: michael@0: private static NodeList findNodes(XPathExpression expr, Object context) throws ParserException { michael@0: try { michael@0: return (NodeList) expr.evaluate(context, XPathConstants.NODESET); michael@0: } catch (XPathException e) { michael@0: throw new ParserException("Unable to find nodes", -1, e); michael@0: } michael@0: } michael@0: michael@0: private static Node findNode(XPathExpression expr, Object context) throws ParserException { michael@0: try { michael@0: return (Node) expr.evaluate(context, XPathConstants.NODE); michael@0: } catch (XPathException e) { michael@0: throw new ParserException("Unable to find node", -1, e); michael@0: } michael@0: } michael@0: michael@0: private static List findElements(XPathExpression expr, Object context) throws ParserException { michael@0: NodeList nodes = findNodes(expr, context); michael@0: ArrayList elements = new ArrayList(); michael@0: for (int i = 0; i < nodes.getLength(); i++) { michael@0: Node n = nodes.item(i); michael@0: if (n instanceof Element) michael@0: elements.add((Element) n); michael@0: } michael@0: return elements; michael@0: } michael@0: michael@0: private static Element findElement(XPathExpression expr, Object context) throws ParserException { michael@0: Node n = findNode(expr, context); michael@0: if (n == null || (!(n instanceof Element))) michael@0: return null; michael@0: return (Element) n; michael@0: } michael@0: michael@0: private static String getTextContent(Element element) throws ParserException { michael@0: try { michael@0: String content = element.getFirstChild().getNodeValue(); michael@0: if (content != null) { michael@0: return content.trim().replaceAll("\\s+", " "); michael@0: } michael@0: return content; michael@0: } catch (DOMException e) { michael@0: throw new ParserException("Unable to get text content for element " + element.getNodeName(), -1, e); michael@0: } michael@0: } michael@0: michael@0: private void buildCalendar(Document d, ContentHandler handler) throws ParserException { michael@0: // "The root class name for hCalendar is "vcalendar". An element with a michael@0: // class name of "vcalendar" is itself called an hCalendar. michael@0: // michael@0: // The root class name for events is "vevent". An element with a class michael@0: // name of "vevent" is itself called an hCalender event. michael@0: // michael@0: // For authoring convenience, both "vevent" and "vcalendar" are michael@0: // treated as root class names for parsing purposes. If a document michael@0: // contains elements with class name "vevent" but not "vcalendar", the michael@0: // entire document has an implied "vcalendar" context." michael@0: michael@0: // XXX: We assume that the entire document has a single vcalendar michael@0: // context. It is possible that the document contains more than one michael@0: // vcalendar element. In this case, we should probably only process michael@0: // that element and log a warning about skipping the others. michael@0: michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Building calendar"); michael@0: michael@0: handler.startCalendar(); michael@0: michael@0: // no PRODID, as the using application should set that itself michael@0: michael@0: handler.startProperty(Property.VERSION); michael@0: try { michael@0: handler.propertyValue(Version.VERSION_2_0.getValue()); michael@0: } catch (Exception e) { michael@0: } michael@0: ; michael@0: handler.endProperty(Property.VERSION); michael@0: michael@0: Element method = findElement(XPATH_METHOD, d); michael@0: if (method != null) { michael@0: buildProperty(method, Property.METHOD, handler); michael@0: } michael@0: michael@0: List vevents = findElements(XPATH_VEVENTS, d); michael@0: for (Iterator i = vevents.iterator(); i.hasNext();) { michael@0: Element vevent = (Element) i.next(); michael@0: buildEvent(vevent, handler); michael@0: } michael@0: michael@0: // XXX: support other "first class components": vjournal, vtodo, michael@0: // vfreebusy, vavailability, vvenue michael@0: michael@0: handler.endCalendar(); michael@0: } michael@0: michael@0: private void buildEvent(Element element, ContentHandler handler) throws ParserException { michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Building event"); michael@0: michael@0: handler.startComponent(Component.VEVENT); michael@0: michael@0: buildProperty(findElement(XPATH_DTSTART, element), Property.DTSTART, handler); michael@0: buildProperty(findElement(XPATH_DTEND, element), Property.DTEND, handler); michael@0: buildProperty(findElement(XPATH_DURATION, element), Property.DURATION, handler); michael@0: buildProperty(findElement(XPATH_SUMMARY, element), Property.SUMMARY, handler); michael@0: buildProperty(findElement(XPATH_UID, element), Property.UID, handler); michael@0: buildProperty(findElement(XPATH_DTSTAMP, element), Property.DTSTAMP, handler); michael@0: List categories = findElements(XPATH_CATEGORY, element); michael@0: for (Iterator i = categories.iterator(); i.hasNext();) { michael@0: Element category = (Element) i.next(); michael@0: buildProperty(category, Property.CATEGORIES, handler); michael@0: } michael@0: buildProperty(findElement(XPATH_LOCATION, element), Property.LOCATION, handler); michael@0: buildProperty(findElement(XPATH_URL, element), Property.URL, handler); michael@0: buildProperty(findElement(XPATH_DESCRIPTION, element), Property.DESCRIPTION, handler); michael@0: buildProperty(findElement(XPATH_LAST_MODIFIED, element), Property.LAST_MODIFIED, handler); michael@0: buildProperty(findElement(XPATH_STATUS, element), Property.STATUS, handler); michael@0: buildProperty(findElement(XPATH_CLASS, element), Property.CLASS, handler); michael@0: List attendees = findElements(XPATH_ATTENDEE, element); michael@0: for (Iterator i = attendees.iterator(); i.hasNext();) { michael@0: Element attendee = (Element) i.next(); michael@0: buildProperty(attendee, Property.ATTENDEE, handler); michael@0: } michael@0: buildProperty(findElement(XPATH_CONTACT, element), Property.CONTACT, handler); michael@0: buildProperty(findElement(XPATH_ORGANIZER, element), Property.ORGANIZER, handler); michael@0: buildProperty(findElement(XPATH_SEQUENCE, element), Property.SEQUENCE, handler); michael@0: buildProperty(findElement(XPATH_ATTACH, element), Property.ATTACH, handler); michael@0: michael@0: handler.endComponent(Component.VEVENT); michael@0: } michael@0: michael@0: private void buildProperty(Element element, String propName, ContentHandler handler) throws ParserException { michael@0: if (element == null) michael@0: return; michael@0: michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Building property " + propName); michael@0: michael@0: String className = className(propName); michael@0: String elementName = element.getLocalName().toLowerCase(); michael@0: michael@0: String value = null; michael@0: if (elementName.equals("abbr")) { michael@0: // "If an element is used for a property, then the 'title' michael@0: // attribute of the element is the value of the property, michael@0: // instead of the contents of the element, which instead provide a michael@0: // human presentable version of the value." michael@0: value = element.getAttribute("title"); michael@0: if (StringUtils.isBlank(value)) michael@0: throw new ParserException("Abbr element '" + className + "' requires a non-empty title", -1); michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from title attribute"); michael@0: } else if (isHeaderElement(elementName)) { michael@0: // try title first. if that's not set, fall back to text content. michael@0: value = element.getAttribute("title"); michael@0: if (!StringUtils.isBlank(value)) { michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from title attribute"); michael@0: } else { michael@0: value = getTextContent(element); michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from text content"); michael@0: } michael@0: } else if (elementName.equals("a") && isUrlProperty(propName)) { michael@0: value = element.getAttribute("href"); michael@0: if (StringUtils.isBlank(value)) michael@0: throw new ParserException("A element '" + className + "' requires a non-empty href", -1); michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from href attribute"); michael@0: } else if (elementName.equals("img")) { michael@0: if (isUrlProperty(propName)) { michael@0: value = element.getAttribute("src"); michael@0: if (StringUtils.isBlank(value)) michael@0: throw new ParserException("Img element '" + className + "' requires a non-empty src", -1); michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from src attribute"); michael@0: } else { michael@0: value = element.getAttribute("alt"); michael@0: if (StringUtils.isBlank(value)) michael@0: throw new ParserException("Img element '" + className + "' requires a non-empty alt", -1); michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from alt attribute"); michael@0: } michael@0: } else { michael@0: value = getTextContent(element); michael@0: if (!StringUtils.isBlank(value)) { michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Setting value '" + value + "' from text content"); michael@0: } michael@0: } michael@0: michael@0: if (StringUtils.isBlank(value)) { michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("Skipping property with empty value"); michael@0: return; michael@0: } michael@0: michael@0: handler.startProperty(propName); michael@0: michael@0: // if it's a date property, we have to convert from the michael@0: // hCalendar-formatted date (RFC 3339) to an iCalendar-formatted date michael@0: if (isDateProperty(propName)) { michael@0: try { michael@0: Date date = icalDate(value); michael@0: value = date.toString(); michael@0: michael@0: if (!(date instanceof DateTime)) michael@0: try { michael@0: handler.parameter(Parameter.VALUE, Value.DATE.getValue()); michael@0: } catch (Exception e) { michael@0: } michael@0: } catch (ParseException e) { michael@0: throw new ParserException("Malformed date value for element '" + className + "'", -1, e); michael@0: } michael@0: } michael@0: michael@0: if (isTextProperty(propName)) { michael@0: String lang = element.getAttributeNS(XMLConstants.XML_NS_URI, "lang"); michael@0: if (!StringUtils.isBlank(lang)) michael@0: try { michael@0: handler.parameter(Parameter.LANGUAGE, lang); michael@0: } catch (Exception e) { michael@0: } michael@0: } michael@0: michael@0: // XXX: other parameters? michael@0: michael@0: try { michael@0: handler.propertyValue(value); michael@0: } catch (URISyntaxException e) { michael@0: throw new ParserException("Malformed URI value for element '" + className + "'", -1, e); michael@0: } catch (ParseException e) { michael@0: throw new ParserException("Malformed value for element '" + className + "'", -1, e); michael@0: } catch (IOException e) { michael@0: throw new CalendarException(e); michael@0: } michael@0: michael@0: handler.endProperty(propName); michael@0: } michael@0: michael@0: // "The basic format of hCalendar is to use iCalendar object/property michael@0: // names in lower-case for class names ..." michael@0: /* michael@0: * private static String _icalName(Element element) { return element.getAttribute("class").toUpperCase(); } michael@0: */ michael@0: michael@0: private static String className(String propName) { michael@0: return propName.toLowerCase(); michael@0: } michael@0: michael@0: private static boolean isHeaderElement(String name) { michael@0: return (name.equals("h1") || name.equals("h2") || name.equals("h3") michael@0: || name.equals("h4") || name.equals("h5") || name michael@0: .equals("h6")); michael@0: } michael@0: michael@0: private static boolean isDateProperty(String name) { michael@0: return (name.equals(Property.DTSTART) || name.equals(Property.DTEND) || name.equals(Property.DTSTAMP) || name michael@0: .equals(Property.LAST_MODIFIED)); michael@0: } michael@0: michael@0: private static boolean isUrlProperty(String name) { michael@0: return (name.equals(Property.URL)); michael@0: } michael@0: michael@0: private static boolean isTextProperty(String name) { michael@0: return (name.equals(Property.SUMMARY) || name.equals(Property.LOCATION) || name.equals(Property.CATEGORIES) michael@0: || name.equals(Property.DESCRIPTION) || name.equals(Property.ATTENDEE) michael@0: || name.equals(Property.CONTACT) || name michael@0: .equals(Property.ORGANIZER)); michael@0: } michael@0: michael@0: private static Date icalDate(String original) throws ParseException { michael@0: // in the real world, some generators use iCalendar formatted michael@0: // dates and date-times, so try parsing those formats first before michael@0: // going to RFC 3339 formats michael@0: michael@0: if (original.indexOf('T') == -1) { michael@0: // date-only michael@0: try { michael@0: // for some reason Date's pattern matches yyyy-MM-dd, so michael@0: // don't check it if we find - michael@0: if (original.indexOf('-') == -1) michael@0: return new Date(original); michael@0: } catch (Exception e) { michael@0: } michael@0: return new Date(HCAL_DATE_FORMAT.parse(original)); michael@0: } michael@0: michael@0: try { michael@0: return new DateTime(original); michael@0: } catch (Exception e) { michael@0: } michael@0: michael@0: // the date-time value can represent its time zone in a few different michael@0: // ways. we have to normalize those to match our pattern. michael@0: michael@0: String normalized = null; michael@0: michael@0: if (LOG.isDebugEnabled()) michael@0: LOG.debug("normalizing date-time " + original); michael@0: michael@0: // 2002-10-09T19:00:00Z michael@0: if (original.charAt(original.length() - 1) == 'Z') { michael@0: normalized = original.replaceAll("Z", "GMT-00:00"); michael@0: } michael@0: // 2002-10-10T00:00:00+05:00 michael@0: else if (original.indexOf("GMT") == -1 michael@0: && (original.charAt(original.length() - 6) == '+' || original.charAt(original.length() - 6) == '-')) { michael@0: String tzId = "GMT" + original.substring(original.length() - 6); michael@0: normalized = original.substring(0, original.length() - 6) + tzId; michael@0: } else { michael@0: // 2002-10-10T00:00:00GMT+05:00 michael@0: normalized = original; michael@0: } michael@0: michael@0: DateTime dt = new DateTime(HCAL_DATE_TIME_FORMAT.parse(normalized)); michael@0: michael@0: // hCalendar does not specify a representation for timezone ids michael@0: // or any other sort of timezone information. the best it does is michael@0: // give us a timezone offset that we can use to convert the local michael@0: // time to UTC. furthermore, it has no representation for floating michael@0: // date-times. therefore, all dates are converted to UTC. michael@0: michael@0: dt.setUtc(true); michael@0: michael@0: return dt; michael@0: } michael@0: }