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.InputStreamReader; michael@0: import java.io.Reader; michael@0: import java.net.URISyntaxException; michael@0: import java.nio.charset.Charset; michael@0: import java.text.ParseException; michael@0: import java.util.ArrayList; michael@0: import java.util.Iterator; michael@0: import java.util.List; michael@0: michael@0: import net.fortuna.ical4j.model.Calendar; michael@0: import net.fortuna.ical4j.model.CalendarException; michael@0: import net.fortuna.ical4j.model.Component; michael@0: import net.fortuna.ical4j.model.ComponentFactory; michael@0: import net.fortuna.ical4j.model.Escapable; michael@0: import net.fortuna.ical4j.model.Parameter; michael@0: import net.fortuna.ical4j.model.ParameterFactory; michael@0: import net.fortuna.ical4j.model.ParameterFactoryRegistry; michael@0: import net.fortuna.ical4j.model.Property; michael@0: import net.fortuna.ical4j.model.PropertyFactory; michael@0: import net.fortuna.ical4j.model.PropertyFactoryRegistry; michael@0: import net.fortuna.ical4j.model.TimeZone; michael@0: import net.fortuna.ical4j.model.TimeZoneRegistry; michael@0: import net.fortuna.ical4j.model.TimeZoneRegistryFactory; michael@0: import net.fortuna.ical4j.model.component.VAvailability; michael@0: import net.fortuna.ical4j.model.component.VEvent; michael@0: import net.fortuna.ical4j.model.component.VTimeZone; michael@0: import net.fortuna.ical4j.model.component.VToDo; michael@0: import net.fortuna.ical4j.model.parameter.TzId; michael@0: import net.fortuna.ical4j.model.property.DateListProperty; michael@0: import net.fortuna.ical4j.model.property.DateProperty; michael@0: import net.fortuna.ical4j.model.property.XProperty; michael@0: import net.fortuna.ical4j.util.CompatibilityHints; michael@0: import net.fortuna.ical4j.util.Constants; michael@0: import net.fortuna.ical4j.util.Strings; michael@0: michael@0: import org.apache.commons.logging.Log; michael@0: import org.apache.commons.logging.LogFactory; michael@0: michael@0: /** michael@0: * Parses and builds an iCalendar model from an input stream. Note that this class is not thread-safe. michael@0: * @version 2.0 michael@0: * @author Ben Fortuna michael@0: * michael@0: *
michael@0: * $Id$ michael@0: * michael@0: * Created: Apr 5, 2004 michael@0: *michael@0: * michael@0: */ michael@0: public class CalendarBuilder { michael@0: michael@0: private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); michael@0: michael@0: private final CalendarParser parser; michael@0: michael@0: private final ContentHandler contentHandler; michael@0: michael@0: private final TimeZoneRegistry tzRegistry; michael@0: michael@0: private List datesMissingTimezones; michael@0: michael@0: /** michael@0: * The calendar instance created by the builder. michael@0: */ michael@0: protected Calendar calendar; michael@0: michael@0: /** michael@0: * The current component instance created by the builder. michael@0: */ michael@0: protected Component component; michael@0: michael@0: /** michael@0: * The current sub-component instance created by the builder. michael@0: */ michael@0: protected Component subComponent; michael@0: michael@0: /** michael@0: * The current property instance created by the builder. michael@0: */ michael@0: protected Property property; michael@0: michael@0: /** michael@0: * Default constructor. michael@0: */ michael@0: public CalendarBuilder() { michael@0: this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), michael@0: new ParameterFactoryRegistry(), TimeZoneRegistryFactory.getInstance().createRegistry()); michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new calendar builder using the specified calendar parser. michael@0: * @param parser a calendar parser used to parse calendar files michael@0: */ michael@0: public CalendarBuilder(final CalendarParser parser) { michael@0: this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), michael@0: TimeZoneRegistryFactory.getInstance().createRegistry()); michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new calendar builder using the specified timezone registry. michael@0: * @param tzRegistry a timezone registry to populate with discovered timezones michael@0: */ michael@0: public CalendarBuilder(final TimeZoneRegistry tzRegistry) { michael@0: this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), michael@0: new ParameterFactoryRegistry(), tzRegistry); michael@0: } michael@0: michael@0: /** michael@0: * Constructs a new instance using the specified parser and registry. michael@0: * @param parser a calendar parser used to construct the calendar michael@0: * @param tzRegistry a timezone registry used to retrieve {@link TimeZone}s and michael@0: * register additional timezone information found michael@0: * in the calendar michael@0: */ michael@0: public CalendarBuilder(CalendarParser parser, TimeZoneRegistry tzRegistry) { michael@0: this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), tzRegistry); michael@0: } michael@0: michael@0: /** michael@0: * @param parser a custom calendar parser michael@0: * @param propertyFactoryRegistry registry for non-standard property factories michael@0: * @param parameterFactoryRegistry registry for non-standard parameter factories michael@0: * @param tzRegistry a custom timezone registry michael@0: */ michael@0: public CalendarBuilder(CalendarParser parser, PropertyFactoryRegistry propertyFactoryRegistry, michael@0: ParameterFactoryRegistry parameterFactoryRegistry, TimeZoneRegistry tzRegistry) { michael@0: michael@0: this.parser = parser; michael@0: this.tzRegistry = tzRegistry; michael@0: this.contentHandler = new ContentHandlerImpl(ComponentFactory.getInstance(), michael@0: propertyFactoryRegistry, parameterFactoryRegistry); michael@0: } michael@0: michael@0: /** michael@0: * Builds an iCalendar model from the specified input stream. michael@0: * @param in an input stream to read calendar data from michael@0: * @return a calendar parsed from the specified input stream michael@0: * @throws IOException where an error occurs reading data from the specified stream michael@0: * @throws ParserException where an error occurs parsing data from the stream michael@0: */ michael@0: public Calendar build(final InputStream in) throws IOException, michael@0: ParserException { michael@0: return build(new InputStreamReader(in, DEFAULT_CHARSET)); michael@0: } michael@0: michael@0: /** michael@0: * Builds an iCalendar model from the specified reader. An
UnfoldingReader
is applied to the
michael@0: * specified reader to ensure the data stream is correctly unfolded where appropriate.
michael@0: * @param in a reader to read calendar data from
michael@0: * @return a calendar parsed from the specified reader
michael@0: * @throws IOException where an error occurs reading data from the specified reader
michael@0: * @throws ParserException where an error occurs parsing data from the reader
michael@0: */
michael@0: public Calendar build(final Reader in) throws IOException, ParserException {
michael@0: return build(new UnfoldingReader(in));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Build an iCalendar model by parsing data from the specified reader.
michael@0: * @param uin an unfolding reader to read data from
michael@0: * @return a calendar parsed from the specified reader
michael@0: * @throws IOException where an error occurs reading data from the specified reader
michael@0: * @throws ParserException where an error occurs parsing data from the reader
michael@0: */
michael@0: public Calendar build(final UnfoldingReader uin) throws IOException,
michael@0: ParserException {
michael@0: // re-initialise..
michael@0: calendar = null;
michael@0: component = null;
michael@0: subComponent = null;
michael@0: property = null;
michael@0: datesMissingTimezones = new ArrayList();
michael@0:
michael@0: parser.parse(uin, contentHandler);
michael@0:
michael@0: if (datesMissingTimezones.size() > 0 && tzRegistry != null) {
michael@0: resolveTimezones();
michael@0: }
michael@0:
michael@0: return calendar;
michael@0: }
michael@0:
michael@0: private class ContentHandlerImpl implements ContentHandler {
michael@0:
michael@0: private final ComponentFactory componentFactory;
michael@0:
michael@0: private final PropertyFactory propertyFactory;
michael@0:
michael@0: private final ParameterFactory parameterFactory;
michael@0:
michael@0: public ContentHandlerImpl(ComponentFactory componentFactory, PropertyFactory propertyFactory,
michael@0: ParameterFactory parameterFactory) {
michael@0:
michael@0: this.componentFactory = componentFactory;
michael@0: this.propertyFactory = propertyFactory;
michael@0: this.parameterFactory = parameterFactory;
michael@0: }
michael@0:
michael@0: public void endCalendar() {
michael@0: // do nothing..
michael@0: }
michael@0:
michael@0: public void endComponent(final String name) {
michael@0: assertComponent(component);
michael@0:
michael@0: if (subComponent != null) {
michael@0: if (component instanceof VTimeZone) {
michael@0: ((VTimeZone) component).getObservances().add(subComponent);
michael@0: }
michael@0: else if (component instanceof VEvent) {
michael@0: ((VEvent) component).getAlarms().add(subComponent);
michael@0: }
michael@0: else if (component instanceof VToDo) {
michael@0: ((VToDo) component).getAlarms().add(subComponent);
michael@0: }
michael@0: else if (component instanceof VAvailability) {
michael@0: ((VAvailability) component).getAvailable().add(subComponent);
michael@0: }
michael@0: subComponent = null;
michael@0: }
michael@0: else {
michael@0: calendar.getComponents().add(component);
michael@0: if (component instanceof VTimeZone && tzRegistry != null) {
michael@0: // register the timezone for use with iCalendar objects..
michael@0: tzRegistry.register(new TimeZone((VTimeZone) component));
michael@0: }
michael@0: component = null;
michael@0: }
michael@0: }
michael@0:
michael@0: public void endProperty(final String name) {
michael@0: assertProperty(property);
michael@0:
michael@0: // replace with a constant instance if applicable..
michael@0: property = Constants.forProperty(property);
michael@0: if (component != null) {
michael@0: if (subComponent != null) {
michael@0: subComponent.getProperties().add(property);
michael@0: }
michael@0: else {
michael@0: component.getProperties().add(property);
michael@0: }
michael@0: }
michael@0: else if (calendar != null) {
michael@0: calendar.getProperties().add(property);
michael@0: }
michael@0:
michael@0: property = null;
michael@0: }
michael@0:
michael@0: public void parameter(final String name, final String value) throws URISyntaxException {
michael@0: assertProperty(property);
michael@0:
michael@0: // parameter names are case-insensitive, but convert to upper case to simplify further processing
michael@0: final Parameter param = parameterFactory.createParameter(name.toUpperCase(), Strings.escapeNewline(value));
michael@0: property.getParameters().add(param);
michael@0: if (param instanceof TzId && tzRegistry != null && !(property instanceof XProperty)) {
michael@0: final TimeZone timezone = tzRegistry.getTimeZone(param.getValue());
michael@0: if (timezone != null) {
michael@0: updateTimeZone(property, timezone);
michael@0: } else {
michael@0: // VTIMEZONE may be defined later, so so keep
michael@0: // track of dates until all components have been
michael@0: // parsed, and then try again later
michael@0: datesMissingTimezones.add(property);
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * {@inheritDoc}
michael@0: */
michael@0: public void propertyValue(final String value) throws URISyntaxException,
michael@0: ParseException, IOException {
michael@0:
michael@0: assertProperty(property);
michael@0:
michael@0: if (property instanceof Escapable) {
michael@0: property.setValue(Strings.unescape(value));
michael@0: }
michael@0: else {
michael@0: property.setValue(value);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * {@inheritDoc}
michael@0: */
michael@0: public void startCalendar() {
michael@0: calendar = new Calendar();
michael@0: }
michael@0:
michael@0: /**
michael@0: * {@inheritDoc}
michael@0: */
michael@0: public void startComponent(final String name) {
michael@0: if (component != null) {
michael@0: subComponent = componentFactory.createComponent(name);
michael@0: }
michael@0: else {
michael@0: component = componentFactory.createComponent(name);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * {@inheritDoc}
michael@0: */
michael@0: public void startProperty(final String name) {
michael@0: // property names are case-insensitive, but convert to upper case to simplify further processing
michael@0: property = propertyFactory.createProperty(name.toUpperCase());
michael@0: }
michael@0: }
michael@0:
michael@0: private void assertComponent(Component component) {
michael@0: if (component == null) {
michael@0: throw new CalendarException("Expected component not initialised");
michael@0: }
michael@0: }
michael@0:
michael@0: private void assertProperty(Property property) {
michael@0: if (property == null) {
michael@0: throw new CalendarException("Expected property not initialised");
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Returns the timezone registry used in the construction of calendars.
michael@0: * @return a timezone registry
michael@0: */
michael@0: public final TimeZoneRegistry getRegistry() {
michael@0: return tzRegistry;
michael@0: }
michael@0:
michael@0: private void updateTimeZone(Property property, TimeZone timezone) {
michael@0: try {
michael@0: ((DateProperty) property).setTimeZone(timezone);
michael@0: }
michael@0: catch (ClassCastException e) {
michael@0: try {
michael@0: ((DateListProperty) property).setTimeZone(timezone);
michael@0: }
michael@0: catch (ClassCastException e2) {
michael@0: if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) {
michael@0: Log log = LogFactory.getLog(CalendarBuilder.class);
michael@0: log.warn("Error setting timezone [" + timezone.getID()
michael@0: + "] on property [" + property.getName()
michael@0: + "]", e);
michael@0: }
michael@0: else {
michael@0: throw e2;
michael@0: }
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: private void resolveTimezones()
michael@0: throws IOException {
michael@0:
michael@0: // Go through each property and try to resolve the TZID.
michael@0: for (final Iterator it = datesMissingTimezones.iterator();it.hasNext();) {
michael@0: final Property property = (Property) it.next();
michael@0: final Parameter tzParam = property.getParameter(Parameter.TZID);
michael@0:
michael@0: // tzParam might be null:
michael@0: if (tzParam == null) {
michael@0: continue;
michael@0: }
michael@0:
michael@0: //lookup timezone
michael@0: final TimeZone timezone = tzRegistry.getTimeZone(tzParam.getValue());
michael@0:
michael@0: // If timezone found, then update date property
michael@0: if (timezone != null) {
michael@0: // Get the String representation of date(s) as
michael@0: // we will need this after changing the timezone
michael@0: final String strDate = property.getValue();
michael@0:
michael@0: // Change the timezone
michael@0: if(property instanceof DateProperty) {
michael@0: ((DateProperty) property).setTimeZone(timezone);
michael@0: }
michael@0: else if(property instanceof DateListProperty) {
michael@0: ((DateListProperty) property).setTimeZone(timezone);
michael@0: }
michael@0:
michael@0: // Reset value
michael@0: try {
michael@0: property.setValue(strDate);
michael@0: } catch (ParseException e) {
michael@0: // shouldn't happen as its already been parsed
michael@0: throw new CalendarException(e);
michael@0: } catch (URISyntaxException e) {
michael@0: // shouldn't happen as its already been parsed
michael@0: throw new CalendarException(e);
michael@0: }
michael@0: }
michael@0: }
michael@0: }
michael@0: }