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