1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/src/net/fortuna/ical4j/data/CalendarBuilder.java Tue Feb 10 18:12:00 2015 +0100 1.3 @@ -0,0 +1,435 @@ 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.InputStreamReader; 1.40 +import java.io.Reader; 1.41 +import java.net.URISyntaxException; 1.42 +import java.nio.charset.Charset; 1.43 +import java.text.ParseException; 1.44 +import java.util.ArrayList; 1.45 +import java.util.Iterator; 1.46 +import java.util.List; 1.47 + 1.48 +import net.fortuna.ical4j.model.Calendar; 1.49 +import net.fortuna.ical4j.model.CalendarException; 1.50 +import net.fortuna.ical4j.model.Component; 1.51 +import net.fortuna.ical4j.model.ComponentFactory; 1.52 +import net.fortuna.ical4j.model.Escapable; 1.53 +import net.fortuna.ical4j.model.Parameter; 1.54 +import net.fortuna.ical4j.model.ParameterFactory; 1.55 +import net.fortuna.ical4j.model.ParameterFactoryRegistry; 1.56 +import net.fortuna.ical4j.model.Property; 1.57 +import net.fortuna.ical4j.model.PropertyFactory; 1.58 +import net.fortuna.ical4j.model.PropertyFactoryRegistry; 1.59 +import net.fortuna.ical4j.model.TimeZone; 1.60 +import net.fortuna.ical4j.model.TimeZoneRegistry; 1.61 +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; 1.62 +import net.fortuna.ical4j.model.component.VAvailability; 1.63 +import net.fortuna.ical4j.model.component.VEvent; 1.64 +import net.fortuna.ical4j.model.component.VTimeZone; 1.65 +import net.fortuna.ical4j.model.component.VToDo; 1.66 +import net.fortuna.ical4j.model.parameter.TzId; 1.67 +import net.fortuna.ical4j.model.property.DateListProperty; 1.68 +import net.fortuna.ical4j.model.property.DateProperty; 1.69 +import net.fortuna.ical4j.model.property.XProperty; 1.70 +import net.fortuna.ical4j.util.CompatibilityHints; 1.71 +import net.fortuna.ical4j.util.Constants; 1.72 +import net.fortuna.ical4j.util.Strings; 1.73 + 1.74 +import org.apache.commons.logging.Log; 1.75 +import org.apache.commons.logging.LogFactory; 1.76 + 1.77 +/** 1.78 + * Parses and builds an iCalendar model from an input stream. Note that this class is not thread-safe. 1.79 + * @version 2.0 1.80 + * @author Ben Fortuna 1.81 + * 1.82 + * <pre> 1.83 + * $Id$ 1.84 + * 1.85 + * Created: Apr 5, 2004 1.86 + * </pre> 1.87 + * 1.88 + */ 1.89 +public class CalendarBuilder { 1.90 + 1.91 + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 1.92 + 1.93 + private final CalendarParser parser; 1.94 + 1.95 + private final ContentHandler contentHandler; 1.96 + 1.97 + private final TimeZoneRegistry tzRegistry; 1.98 + 1.99 + private List datesMissingTimezones; 1.100 + 1.101 + /** 1.102 + * The calendar instance created by the builder. 1.103 + */ 1.104 + protected Calendar calendar; 1.105 + 1.106 + /** 1.107 + * The current component instance created by the builder. 1.108 + */ 1.109 + protected Component component; 1.110 + 1.111 + /** 1.112 + * The current sub-component instance created by the builder. 1.113 + */ 1.114 + protected Component subComponent; 1.115 + 1.116 + /** 1.117 + * The current property instance created by the builder. 1.118 + */ 1.119 + protected Property property; 1.120 + 1.121 + /** 1.122 + * Default constructor. 1.123 + */ 1.124 + public CalendarBuilder() { 1.125 + this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), 1.126 + new ParameterFactoryRegistry(), TimeZoneRegistryFactory.getInstance().createRegistry()); 1.127 + } 1.128 + 1.129 + /** 1.130 + * Constructs a new calendar builder using the specified calendar parser. 1.131 + * @param parser a calendar parser used to parse calendar files 1.132 + */ 1.133 + public CalendarBuilder(final CalendarParser parser) { 1.134 + this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), 1.135 + TimeZoneRegistryFactory.getInstance().createRegistry()); 1.136 + } 1.137 + 1.138 + /** 1.139 + * Constructs a new calendar builder using the specified timezone registry. 1.140 + * @param tzRegistry a timezone registry to populate with discovered timezones 1.141 + */ 1.142 + public CalendarBuilder(final TimeZoneRegistry tzRegistry) { 1.143 + this(CalendarParserFactory.getInstance().createParser(), new PropertyFactoryRegistry(), 1.144 + new ParameterFactoryRegistry(), tzRegistry); 1.145 + } 1.146 + 1.147 + /** 1.148 + * Constructs a new instance using the specified parser and registry. 1.149 + * @param parser a calendar parser used to construct the calendar 1.150 + * @param tzRegistry a timezone registry used to retrieve {@link TimeZone}s and 1.151 + * register additional timezone information found 1.152 + * in the calendar 1.153 + */ 1.154 + public CalendarBuilder(CalendarParser parser, TimeZoneRegistry tzRegistry) { 1.155 + this(parser, new PropertyFactoryRegistry(), new ParameterFactoryRegistry(), tzRegistry); 1.156 + } 1.157 + 1.158 + /** 1.159 + * @param parser a custom calendar parser 1.160 + * @param propertyFactoryRegistry registry for non-standard property factories 1.161 + * @param parameterFactoryRegistry registry for non-standard parameter factories 1.162 + * @param tzRegistry a custom timezone registry 1.163 + */ 1.164 + public CalendarBuilder(CalendarParser parser, PropertyFactoryRegistry propertyFactoryRegistry, 1.165 + ParameterFactoryRegistry parameterFactoryRegistry, TimeZoneRegistry tzRegistry) { 1.166 + 1.167 + this.parser = parser; 1.168 + this.tzRegistry = tzRegistry; 1.169 + this.contentHandler = new ContentHandlerImpl(ComponentFactory.getInstance(), 1.170 + propertyFactoryRegistry, parameterFactoryRegistry); 1.171 + } 1.172 + 1.173 + /** 1.174 + * Builds an iCalendar model from the specified input stream. 1.175 + * @param in an input stream to read calendar data from 1.176 + * @return a calendar parsed from the specified input stream 1.177 + * @throws IOException where an error occurs reading data from the specified stream 1.178 + * @throws ParserException where an error occurs parsing data from the stream 1.179 + */ 1.180 + public Calendar build(final InputStream in) throws IOException, 1.181 + ParserException { 1.182 + return build(new InputStreamReader(in, DEFAULT_CHARSET)); 1.183 + } 1.184 + 1.185 + /** 1.186 + * Builds an iCalendar model from the specified reader. An <code>UnfoldingReader</code> is applied to the 1.187 + * specified reader to ensure the data stream is correctly unfolded where appropriate. 1.188 + * @param in a reader to read calendar data from 1.189 + * @return a calendar parsed from the specified reader 1.190 + * @throws IOException where an error occurs reading data from the specified reader 1.191 + * @throws ParserException where an error occurs parsing data from the reader 1.192 + */ 1.193 + public Calendar build(final Reader in) throws IOException, ParserException { 1.194 + return build(new UnfoldingReader(in)); 1.195 + } 1.196 + 1.197 + /** 1.198 + * Build an iCalendar model by parsing data from the specified reader. 1.199 + * @param uin an unfolding reader to read data from 1.200 + * @return a calendar parsed from the specified reader 1.201 + * @throws IOException where an error occurs reading data from the specified reader 1.202 + * @throws ParserException where an error occurs parsing data from the reader 1.203 + */ 1.204 + public Calendar build(final UnfoldingReader uin) throws IOException, 1.205 + ParserException { 1.206 + // re-initialise.. 1.207 + calendar = null; 1.208 + component = null; 1.209 + subComponent = null; 1.210 + property = null; 1.211 + datesMissingTimezones = new ArrayList(); 1.212 + 1.213 + parser.parse(uin, contentHandler); 1.214 + 1.215 + if (datesMissingTimezones.size() > 0 && tzRegistry != null) { 1.216 + resolveTimezones(); 1.217 + } 1.218 + 1.219 + return calendar; 1.220 + } 1.221 + 1.222 + private class ContentHandlerImpl implements ContentHandler { 1.223 + 1.224 + private final ComponentFactory componentFactory; 1.225 + 1.226 + private final PropertyFactory propertyFactory; 1.227 + 1.228 + private final ParameterFactory parameterFactory; 1.229 + 1.230 + public ContentHandlerImpl(ComponentFactory componentFactory, PropertyFactory propertyFactory, 1.231 + ParameterFactory parameterFactory) { 1.232 + 1.233 + this.componentFactory = componentFactory; 1.234 + this.propertyFactory = propertyFactory; 1.235 + this.parameterFactory = parameterFactory; 1.236 + } 1.237 + 1.238 + public void endCalendar() { 1.239 + // do nothing.. 1.240 + } 1.241 + 1.242 + public void endComponent(final String name) { 1.243 + assertComponent(component); 1.244 + 1.245 + if (subComponent != null) { 1.246 + if (component instanceof VTimeZone) { 1.247 + ((VTimeZone) component).getObservances().add(subComponent); 1.248 + } 1.249 + else if (component instanceof VEvent) { 1.250 + ((VEvent) component).getAlarms().add(subComponent); 1.251 + } 1.252 + else if (component instanceof VToDo) { 1.253 + ((VToDo) component).getAlarms().add(subComponent); 1.254 + } 1.255 + else if (component instanceof VAvailability) { 1.256 + ((VAvailability) component).getAvailable().add(subComponent); 1.257 + } 1.258 + subComponent = null; 1.259 + } 1.260 + else { 1.261 + calendar.getComponents().add(component); 1.262 + if (component instanceof VTimeZone && tzRegistry != null) { 1.263 + // register the timezone for use with iCalendar objects.. 1.264 + tzRegistry.register(new TimeZone((VTimeZone) component)); 1.265 + } 1.266 + component = null; 1.267 + } 1.268 + } 1.269 + 1.270 + public void endProperty(final String name) { 1.271 + assertProperty(property); 1.272 + 1.273 + // replace with a constant instance if applicable.. 1.274 + property = Constants.forProperty(property); 1.275 + if (component != null) { 1.276 + if (subComponent != null) { 1.277 + subComponent.getProperties().add(property); 1.278 + } 1.279 + else { 1.280 + component.getProperties().add(property); 1.281 + } 1.282 + } 1.283 + else if (calendar != null) { 1.284 + calendar.getProperties().add(property); 1.285 + } 1.286 + 1.287 + property = null; 1.288 + } 1.289 + 1.290 + public void parameter(final String name, final String value) throws URISyntaxException { 1.291 + assertProperty(property); 1.292 + 1.293 + // parameter names are case-insensitive, but convert to upper case to simplify further processing 1.294 + final Parameter param = parameterFactory.createParameter(name.toUpperCase(), Strings.escapeNewline(value)); 1.295 + property.getParameters().add(param); 1.296 + if (param instanceof TzId && tzRegistry != null && !(property instanceof XProperty)) { 1.297 + final TimeZone timezone = tzRegistry.getTimeZone(param.getValue()); 1.298 + if (timezone != null) { 1.299 + updateTimeZone(property, timezone); 1.300 + } else { 1.301 + // VTIMEZONE may be defined later, so so keep 1.302 + // track of dates until all components have been 1.303 + // parsed, and then try again later 1.304 + datesMissingTimezones.add(property); 1.305 + } 1.306 + } 1.307 + } 1.308 + 1.309 + /** 1.310 + * {@inheritDoc} 1.311 + */ 1.312 + public void propertyValue(final String value) throws URISyntaxException, 1.313 + ParseException, IOException { 1.314 + 1.315 + assertProperty(property); 1.316 + 1.317 + if (property instanceof Escapable) { 1.318 + property.setValue(Strings.unescape(value)); 1.319 + } 1.320 + else { 1.321 + property.setValue(value); 1.322 + } 1.323 + } 1.324 + 1.325 + /** 1.326 + * {@inheritDoc} 1.327 + */ 1.328 + public void startCalendar() { 1.329 + calendar = new Calendar(); 1.330 + } 1.331 + 1.332 + /** 1.333 + * {@inheritDoc} 1.334 + */ 1.335 + public void startComponent(final String name) { 1.336 + if (component != null) { 1.337 + subComponent = componentFactory.createComponent(name); 1.338 + } 1.339 + else { 1.340 + component = componentFactory.createComponent(name); 1.341 + } 1.342 + } 1.343 + 1.344 + /** 1.345 + * {@inheritDoc} 1.346 + */ 1.347 + public void startProperty(final String name) { 1.348 + // property names are case-insensitive, but convert to upper case to simplify further processing 1.349 + property = propertyFactory.createProperty(name.toUpperCase()); 1.350 + } 1.351 + } 1.352 + 1.353 + private void assertComponent(Component component) { 1.354 + if (component == null) { 1.355 + throw new CalendarException("Expected component not initialised"); 1.356 + } 1.357 + } 1.358 + 1.359 + private void assertProperty(Property property) { 1.360 + if (property == null) { 1.361 + throw new CalendarException("Expected property not initialised"); 1.362 + } 1.363 + } 1.364 + 1.365 + /** 1.366 + * Returns the timezone registry used in the construction of calendars. 1.367 + * @return a timezone registry 1.368 + */ 1.369 + public final TimeZoneRegistry getRegistry() { 1.370 + return tzRegistry; 1.371 + } 1.372 + 1.373 + private void updateTimeZone(Property property, TimeZone timezone) { 1.374 + try { 1.375 + ((DateProperty) property).setTimeZone(timezone); 1.376 + } 1.377 + catch (ClassCastException e) { 1.378 + try { 1.379 + ((DateListProperty) property).setTimeZone(timezone); 1.380 + } 1.381 + catch (ClassCastException e2) { 1.382 + if (CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING)) { 1.383 + Log log = LogFactory.getLog(CalendarBuilder.class); 1.384 + log.warn("Error setting timezone [" + timezone.getID() 1.385 + + "] on property [" + property.getName() 1.386 + + "]", e); 1.387 + } 1.388 + else { 1.389 + throw e2; 1.390 + } 1.391 + } 1.392 + } 1.393 + } 1.394 + 1.395 + private void resolveTimezones() 1.396 + throws IOException { 1.397 + 1.398 + // Go through each property and try to resolve the TZID. 1.399 + for (final Iterator it = datesMissingTimezones.iterator();it.hasNext();) { 1.400 + final Property property = (Property) it.next(); 1.401 + final Parameter tzParam = property.getParameter(Parameter.TZID); 1.402 + 1.403 + // tzParam might be null: 1.404 + if (tzParam == null) { 1.405 + continue; 1.406 + } 1.407 + 1.408 + //lookup timezone 1.409 + final TimeZone timezone = tzRegistry.getTimeZone(tzParam.getValue()); 1.410 + 1.411 + // If timezone found, then update date property 1.412 + if (timezone != null) { 1.413 + // Get the String representation of date(s) as 1.414 + // we will need this after changing the timezone 1.415 + final String strDate = property.getValue(); 1.416 + 1.417 + // Change the timezone 1.418 + if(property instanceof DateProperty) { 1.419 + ((DateProperty) property).setTimeZone(timezone); 1.420 + } 1.421 + else if(property instanceof DateListProperty) { 1.422 + ((DateListProperty) property).setTimeZone(timezone); 1.423 + } 1.424 + 1.425 + // Reset value 1.426 + try { 1.427 + property.setValue(strDate); 1.428 + } catch (ParseException e) { 1.429 + // shouldn't happen as its already been parsed 1.430 + throw new CalendarException(e); 1.431 + } catch (URISyntaxException e) { 1.432 + // shouldn't happen as its already been parsed 1.433 + throw new CalendarException(e); 1.434 + } 1.435 + } 1.436 + } 1.437 + } 1.438 +}