001 package biweekly.io.text;
002
003 import static biweekly.util.IOUtils.utf8Reader;
004
005 import java.io.Closeable;
006 import java.io.File;
007 import java.io.FileNotFoundException;
008 import java.io.IOException;
009 import java.io.InputStream;
010 import java.io.Reader;
011 import java.io.StringReader;
012 import java.util.ArrayList;
013 import java.util.List;
014
015 import biweekly.ICalDataType;
016 import biweekly.ICalendar;
017 import biweekly.Messages;
018 import biweekly.Warning;
019 import biweekly.component.ICalComponent;
020 import biweekly.component.marshaller.ICalComponentMarshaller;
021 import biweekly.component.marshaller.ICalendarMarshaller;
022 import biweekly.io.CannotParseException;
023 import biweekly.io.ICalMarshallerRegistrar;
024 import biweekly.io.SkipMeException;
025 import biweekly.parameter.ICalParameters;
026 import biweekly.property.ICalProperty;
027 import biweekly.property.RawProperty;
028 import biweekly.property.marshaller.ICalPropertyMarshaller;
029 import biweekly.property.marshaller.ICalPropertyMarshaller.Result;
030
031 /*
032 Copyright (c) 2013, Michael Angstadt
033 All rights reserved.
034
035 Redistribution and use in source and binary forms, with or without
036 modification, are permitted provided that the following conditions are met:
037
038 1. Redistributions of source code must retain the above copyright notice, this
039 list of conditions and the following disclaimer.
040 2. Redistributions in binary form must reproduce the above copyright notice,
041 this list of conditions and the following disclaimer in the documentation
042 and/or other materials provided with the distribution.
043
044 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
045 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
046 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
047 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
048 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
049 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
050 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
052 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
053 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
054 */
055
056 /**
057 * <p>
058 * Parses {@link ICalendar} objects from an iCalendar data stream.
059 * </p>
060 * <p>
061 * <b>Example:</b>
062 *
063 * <pre class="brush:java">
064 * InputStream in = ...
065 * ICalReader icalReader = new ICalReader(in);
066 * ICalendar ical;
067 * while ((ical = icalReader.readNext()) != null){
068 * ...
069 * }
070 * icalReader.close();
071 * </pre>
072 *
073 * </p>
074 * @author Michael Angstadt
075 * @rfc 5545
076 */
077 public class ICalReader implements Closeable {
078 private static final ICalendarMarshaller icalMarshaller = ICalMarshallerRegistrar.getICalendarMarshaller();
079 private final List<String> warnings = new ArrayList<String>();
080 private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
081 private final ICalRawReader reader;
082
083 /**
084 * Creates a reader that parses iCalendar objects from a string.
085 * @param string the string
086 */
087 public ICalReader(String string) {
088 this(new StringReader(string));
089 }
090
091 /**
092 * Creates a reader that parses iCalendar objects from an input stream.
093 * @param in the input stream
094 */
095 public ICalReader(InputStream in) {
096 this(utf8Reader(in));
097 }
098
099 /**
100 * Creates a reader that parses iCalendar objects from a file.
101 * @param file the file
102 * @throws FileNotFoundException if the file doesn't exist
103 */
104 public ICalReader(File file) throws FileNotFoundException {
105 this(utf8Reader(file));
106 }
107
108 /**
109 * Creates a reader that parses iCalendar objects from a reader.
110 * @param reader the reader
111 */
112 public ICalReader(Reader reader) {
113 this.reader = new ICalRawReader(reader);
114 }
115
116 /**
117 * Gets whether the reader will decode parameter values that use circumflex
118 * accent encoding (enabled by default). This escaping mechanism allows
119 * newlines and double quotes to be included in parameter values.
120 * @return true if circumflex accent decoding is enabled, false if not
121 * @see ICalRawReader#isCaretDecodingEnabled()
122 */
123 public boolean isCaretDecodingEnabled() {
124 return reader.isCaretDecodingEnabled();
125 }
126
127 /**
128 * Sets whether the reader will decode parameter values that use circumflex
129 * accent encoding (enabled by default). This escaping mechanism allows
130 * newlines and double quotes to be included in parameter values.
131 * @param enable true to use circumflex accent decoding, false not to
132 * @see ICalRawReader#setCaretDecodingEnabled(boolean)
133 */
134 public void setCaretDecodingEnabled(boolean enable) {
135 reader.setCaretDecodingEnabled(enable);
136 }
137
138 /**
139 * <p>
140 * Registers an experimental property marshaller. Can also be used to
141 * override the marshaller of a standard property (such as DTSTART). Calling
142 * this method is the same as calling:
143 * </p>
144 * <p>
145 * {@code getRegistrar().register(marshaller)}.
146 * </p>
147 * @param marshaller the marshaller to register
148 */
149 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
150 registrar.register(marshaller);
151 }
152
153 /**
154 * <p>
155 * Registers an experimental component marshaller. Can also be used to
156 * override the marshaller of a standard component (such as VEVENT). Calling
157 * this method is the same as calling:
158 * </p>
159 * <p>
160 * {@code getRegistrar().register(marshaller)}.
161 * </p>
162 * @param marshaller the marshaller to register
163 */
164 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
165 registrar.register(marshaller);
166 }
167
168 /**
169 * Gets the object that manages the component/property marshaller objects.
170 * @return the marshaller registrar
171 */
172 public ICalMarshallerRegistrar getRegistrar() {
173 return registrar;
174 }
175
176 /**
177 * Sets the object that manages the component/property marshaller objects.
178 * @param registrar the marshaller registrar
179 */
180 public void setRegistrar(ICalMarshallerRegistrar registrar) {
181 this.registrar = registrar;
182 }
183
184 /**
185 * Gets the warnings from the last iCalendar object that was unmarshalled.
186 * This list is reset every time a new iCalendar object is read.
187 * @return the warnings or empty list if there were no warnings
188 */
189 public List<String> getWarnings() {
190 return new ArrayList<String>(warnings);
191 }
192
193 /**
194 * Reads the next iCalendar object.
195 * @return the next iCalendar object or null if there are no more
196 * @throws IOException if there's a problem reading from the stream
197 */
198 public ICalendar readNext() throws IOException {
199 if (reader.eof()) {
200 return null;
201 }
202
203 warnings.clear();
204
205 ICalDataStreamListenerImpl listener = new ICalDataStreamListenerImpl();
206 reader.start(listener);
207
208 if (!listener.dataWasRead) {
209 //EOF was reached without reading anything
210 return null;
211 }
212
213 ICalendar ical;
214 if (listener.orphanedComponents.isEmpty()) {
215 //there were no components in the iCalendar object
216 ical = icalMarshaller.emptyInstance();
217 } else {
218 ICalComponent first = listener.orphanedComponents.get(0);
219 if (first instanceof ICalendar) {
220 //this is the code-path that valid iCalendar objects should reach
221 ical = (ICalendar) first;
222 } else {
223 ical = icalMarshaller.emptyInstance();
224 for (ICalComponent component : listener.orphanedComponents) {
225 ical.addComponent(component);
226 }
227 }
228 }
229
230 //add any properties that were not part of a component (will never happen if the iCalendar object is valid)
231 for (ICalProperty property : listener.orphanedProperties) {
232 ical.addProperty(property);
233 }
234
235 return ical;
236 }
237
238 //TODO how to unmarshal the alarm components (a different class should be created, depending on the ACTION property)
239 //TODO buffer properties in a list before the component class is created
240 private class ICalDataStreamListenerImpl implements ICalRawReader.ICalDataStreamListener {
241 private final String icalComponentName = icalMarshaller.getComponentName();
242
243 private List<ICalProperty> orphanedProperties = new ArrayList<ICalProperty>();
244 private List<ICalComponent> orphanedComponents = new ArrayList<ICalComponent>();
245
246 private List<ICalComponent> componentStack = new ArrayList<ICalComponent>();
247 private List<String> componentNamesStack = new ArrayList<String>();
248 private boolean dataWasRead = false;
249
250 public void beginComponent(String name) {
251 dataWasRead = true;
252
253 ICalComponent parentComponent = getCurrentComponent();
254
255 ICalComponentMarshaller<? extends ICalComponent> m = registrar.getComponentMarshaller(name);
256 ICalComponent component = m.emptyInstance();
257 componentStack.add(component);
258 componentNamesStack.add(name);
259
260 if (parentComponent == null) {
261 orphanedComponents.add(component);
262 } else {
263 parentComponent.addComponent(component);
264 }
265 }
266
267 public void readProperty(String name, ICalParameters parameters, String value) {
268 dataWasRead = true;
269
270 ICalPropertyMarshaller<? extends ICalProperty> m = registrar.getPropertyMarshaller(name);
271
272 //get the data type
273 ICalDataType dataType = parameters.getValue();
274 if (dataType == null) {
275 //use the default data type if there is no VALUE parameter
276 dataType = m.getDefaultDataType();
277 } else {
278 //remove VALUE parameter if it is set
279 parameters.setValue(null);
280 }
281
282 ICalProperty property = null;
283 try {
284 Result<? extends ICalProperty> result = m.parseText(value, dataType, parameters);
285
286 for (Warning warning : result.getWarnings()) {
287 addWarning(name, warning);
288 }
289
290 property = result.getProperty();
291 } catch (SkipMeException e) {
292 addWarning(name, Warning.parse(0, e.getMessage()));
293 } catch (CannotParseException e) {
294 addWarning(name, Warning.parse(1, value, e.getMessage()));
295 property = new RawProperty(name, dataType, value);
296 }
297
298 if (property != null) {
299 ICalComponent parentComponent = getCurrentComponent();
300 if (parentComponent == null) {
301 orphanedProperties.add(property);
302 } else {
303 parentComponent.addProperty(property);
304 }
305 }
306 }
307
308 public void endComponent(String name) {
309 //stop reading when "END:VCALENDAR" is reached
310 if (icalComponentName.equalsIgnoreCase(name)) {
311 throw new ICalRawReader.StopReadingException();
312 }
313
314 //find the component that this END property matches up with
315 int popIndex = -1;
316 for (int i = componentStack.size() - 1; i >= 0; i--) {
317 String n = componentNamesStack.get(i);
318 if (n.equalsIgnoreCase(name)) {
319 popIndex = i;
320 break;
321 }
322 }
323 if (popIndex == -1) {
324 //END property does not match up with any BEGIN properties, so ignore
325 addWarning("END", Warning.parse(2));
326 return;
327 }
328
329 componentStack.subList(popIndex, componentStack.size()).clear();
330 componentNamesStack.subList(popIndex, componentNamesStack.size()).clear();
331 }
332
333 public void invalidLine(String line) {
334 addWarning(null, Warning.parse(3, line));
335 }
336
337 public void valuelessParameter(String propertyName, String parameterName) {
338 addWarning(propertyName, Warning.parse(4, parameterName));
339 }
340
341 private ICalComponent getCurrentComponent() {
342 if (componentStack.isEmpty()) {
343 return null;
344 }
345 return componentStack.get(componentStack.size() - 1);
346 }
347 }
348
349 private void addWarning(String propertyName, Warning warning) {
350 String key = (propertyName == null) ? "parse.line" : "parse.lineWithProp";
351 int line = reader.getLineNum();
352
353 String message = Messages.INSTANCE.getMessage(key, line, warning, propertyName);
354 warnings.add(message);
355 }
356
357 /**
358 * Closes the underlying {@link Reader} object.
359 */
360 //@Override
361 public void close() throws IOException {
362 reader.close();
363 }
364 }