diff -r 000000000000 -r fb9019fb1bf7 src/net/fortuna/ical4j/data/CalendarBuilder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/net/fortuna/ical4j/data/CalendarBuilder.java Tue Feb 10 18:12:00 2015 +0100 @@ -0,0 +1,435 @@ +/** + * Copyright (c) 2012, Ben Fortuna + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * o Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * o Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * o Neither the name of Ben Fortuna nor the names of any other contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.fortuna.ical4j.data; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.CalendarException; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.ComponentFactory; +import net.fortuna.ical4j.model.Escapable; +import net.fortuna.ical4j.model.Parameter; +import net.fortuna.ical4j.model.ParameterFactory; +import net.fortuna.ical4j.model.ParameterFactoryRegistry; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.PropertyFactory; +import net.fortuna.ical4j.model.PropertyFactoryRegistry; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; +import net.fortuna.ical4j.model.component.VAvailability; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.component.VToDo; +import net.fortuna.ical4j.model.parameter.TzId; +import net.fortuna.ical4j.model.property.DateListProperty; +import net.fortuna.ical4j.model.property.DateProperty; +import net.fortuna.ical4j.model.property.XProperty; +import net.fortuna.ical4j.util.CompatibilityHints; +import net.fortuna.ical4j.util.Constants; +import net.fortuna.ical4j.util.Strings; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Parses and builds an iCalendar model from an input stream. Note that this class is not thread-safe. + * @version 2.0 + * @author Ben Fortuna + * + *
+ * $Id$
+ *
+ * Created: Apr 5, 2004
+ * 
+ * + */ +public class CalendarBuilder { + + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final CalendarParser parser; + + private final ContentHandler contentHandler; + + private final TimeZoneRegistry tzRegistry; + + private List datesMissingTimezones; + + /** + * The calendar instance created by the builder. + */ + protected Calendar calendar; + + /** + * The current component instance created by the builder. + */ + protected Component component; + + /** + * The current sub-component instance created by the builder. + */ + protected Component subComponent; + + /** + * The current property instance created by the builder. + */ + protected Property property; + + /** + * Default constructor. + */ + public CalendarBuilder() { + this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), + new ParameterFactoryRegistry(), TimeZoneRegistryFactory.getInstance().createRegistry()); + } + + /** + * Constructs a new calendar builder using the specified calendar parser. + * @param parser a calendar parser used to parse calendar files + */ + public CalendarBuilder(final CalendarParser parser) { + this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), + TimeZoneRegistryFactory.getInstance().createRegistry()); + } + + /** + * Constructs a new calendar builder using the specified timezone registry. + * @param tzRegistry a timezone registry to populate with discovered timezones + */ + public CalendarBuilder(final TimeZoneRegistry tzRegistry) { + this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), + new ParameterFactoryRegistry(), tzRegistry); + } + + /** + * Constructs a new instance using the specified parser and registry. + * @param parser a calendar parser used to construct the calendar + * @param tzRegistry a timezone registry used to retrieve {@link TimeZone}s and + * register additional timezone information found + * in the calendar + */ + public CalendarBuilder(CalendarParser parser, TimeZoneRegistry tzRegistry) { + this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), tzRegistry); + } + + /** + * @param parser a custom calendar parser + * @param propertyFactoryRegistry registry for non-standard property factories + * @param parameterFactoryRegistry registry for non-standard parameter factories + * @param tzRegistry a custom timezone registry + */ + public CalendarBuilder(CalendarParser parser, PropertyFactoryRegistry propertyFactoryRegistry, + ParameterFactoryRegistry parameterFactoryRegistry, TimeZoneRegistry tzRegistry) { + + this.parser = parser; + this.tzRegistry = tzRegistry; + this.contentHandler = new ContentHandlerImpl(ComponentFactory.getInstance(), + propertyFactoryRegistry, parameterFactoryRegistry); + } + + /** + * Builds an iCalendar model from the specified input stream. + * @param in an input stream to read calendar data from + * @return a calendar parsed from the specified input stream + * @throws IOException where an error occurs reading data from the specified stream + * @throws ParserException where an error occurs parsing data from the stream + */ + public Calendar build(final InputStream in) throws IOException, + ParserException { + return build(new InputStreamReader(in, DEFAULT_CHARSET)); + } + + /** + * Builds an iCalendar model from the specified reader. An UnfoldingReader is applied to the + * specified reader to ensure the data stream is correctly unfolded where appropriate. + * @param in a reader to read calendar data from + * @return a calendar parsed from the specified reader + * @throws IOException where an error occurs reading data from the specified reader + * @throws ParserException where an error occurs parsing data from the reader + */ + public Calendar build(final Reader in) throws IOException, ParserException { + return build(new UnfoldingReader(in)); + } + + /** + * Build an iCalendar model by parsing data from the specified reader. + * @param uin an unfolding reader to read data from + * @return a calendar parsed from the specified reader + * @throws IOException where an error occurs reading data from the specified reader + * @throws ParserException where an error occurs parsing data from the reader + */ + public Calendar build(final UnfoldingReader uin) throws IOException, + ParserException { + // re-initialise.. + calendar = null; + component = null; + subComponent = null; + property = null; + datesMissingTimezones = new ArrayList(); + + parser.parse(uin, contentHandler); + + if (datesMissingTimezones.size() > 0 && tzRegistry != null) { + resolveTimezones(); + } + + return calendar; + } + + private class ContentHandlerImpl implements ContentHandler { + + private final ComponentFactory componentFactory; + + private final PropertyFactory propertyFactory; + + private final ParameterFactory parameterFactory; + + public ContentHandlerImpl(ComponentFactory componentFactory, PropertyFactory propertyFactory, + ParameterFactory parameterFactory) { + + this.componentFactory = componentFactory; + this.propertyFactory = propertyFactory; + this.parameterFactory = parameterFactory; + } + + public void endCalendar() { + // do nothing.. + } + + public void endComponent(final String name) { + assertComponent(component); + + if (subComponent != null) { + if (component instanceof VTimeZone) { + ((VTimeZone) component).getObservances().add(subComponent); + } + else if (component instanceof VEvent) { + ((VEvent) component).getAlarms().add(subComponent); + } + else if (component instanceof VToDo) { + ((VToDo) component).getAlarms().add(subComponent); + } + else if (component instanceof VAvailability) { + ((VAvailability) component).getAvailable().add(subComponent); + } + subComponent = null; + } + else { + calendar.getComponents().add(component); + if (component instanceof VTimeZone && tzRegistry != null) { + // register the timezone for use with iCalendar objects.. + tzRegistry.register(new TimeZone((VTimeZone) component)); + } + component = null; + } + } + + public void endProperty(final String name) { + assertProperty(property); + + // replace with a constant instance if applicable.. + property = Constants.forProperty(property); + if (component != null) { + if (subComponent != null) { + subComponent.getProperties().add(property); + } + else { + component.getProperties().add(property); + } + } + else if (calendar != null) { + calendar.getProperties().add(property); + } + + property = null; + } + + public void parameter(final String name, final String value) throws URISyntaxException { + assertProperty(property); + + // parameter names are case-insensitive, but convert to upper case to simplify further processing + final Parameter param = parameterFactory.createParameter(name.toUpperCase(), Strings.escapeNewline(value)); + property.getParameters().add(param); + if (param instanceof TzId && tzRegistry != null && !(property instanceof XProperty)) { + final TimeZone timezone = tzRegistry.getTimeZone(param.getValue()); + if (timezone != null) { + updateTimeZone(property, timezone); + } else { + // VTIMEZONE may be defined later, so so keep + // track of dates until all components have been + // parsed, and then try again later + datesMissingTimezones.add(property); + } + } + } + + /** + * {@inheritDoc} + */ + public void propertyValue(final String value) throws URISyntaxException, + ParseException, IOException { + + assertProperty(property); + + if (property instanceof Escapable) { + property.setValue(Strings.unescape(value)); + } + else { + property.setValue(value); + } + } + + /** + * {@inheritDoc} + */ + public void startCalendar() { + calendar = new Calendar(); + } + + /** + * {@inheritDoc} + */ + public void startComponent(final String name) { + if (component != null) { + subComponent = componentFactory.createComponent(name); + } + else { + component = componentFactory.createComponent(name); + } + } + + /** + * {@inheritDoc} + */ + public void startProperty(final String name) { + // property names are case-insensitive, but convert to upper case to simplify further processing + property = propertyFactory.createProperty(name.toUpperCase()); + } + } + + private void assertComponent(Component component) { + if (component == null) { + throw new CalendarException("Expected component not initialised"); + } + } + + private void assertProperty(Property property) { + if (property == null) { + throw new CalendarException("Expected property not initialised"); + } + } + + /** + * Returns the timezone registry used in the construction of calendars. + * @return a timezone registry + */ + public final TimeZoneRegistry getRegistry() { + return tzRegistry; + } + + private void updateTimeZone(Property property, TimeZone timezone) { + try { + ((DateProperty) property).setTimeZone(timezone); + } + catch (ClassCastException e) { + try { + ((DateListProperty) property).setTimeZone(timezone); + } + catch (ClassCastException e2) { + if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) { + Log log = LogFactory.getLog(CalendarBuilder.class); + log.warn("Error setting timezone [" + timezone.getID() + + "] on property [" + property.getName() + + "]", e); + } + else { + throw e2; + } + } + } + } + + private void resolveTimezones() + throws IOException { + + // Go through each property and try to resolve the TZID. + for (final Iterator it = datesMissingTimezones.iterator();it.hasNext();) { + final Property property = (Property) it.next(); + final Parameter tzParam = property.getParameter(Parameter.TZID); + + // tzParam might be null: + if (tzParam == null) { + continue; + } + + //lookup timezone + final TimeZone timezone = tzRegistry.getTimeZone(tzParam.getValue()); + + // If timezone found, then update date property + if (timezone != null) { + // Get the String representation of date(s) as + // we will need this after changing the timezone + final String strDate = property.getValue(); + + // Change the timezone + if(property instanceof DateProperty) { + ((DateProperty) property).setTimeZone(timezone); + } + else if(property instanceof DateListProperty) { + ((DateListProperty) property).setTimeZone(timezone); + } + + // Reset value + try { + property.setValue(strDate); + } catch (ParseException e) { + // shouldn't happen as its already been parsed + throw new CalendarException(e); + } catch (URISyntaxException e) { + // shouldn't happen as its already been parsed + throw new CalendarException(e); + } + } + } + } +}