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