001 package biweekly.io.text;
002
003 import java.io.Closeable;
004 import java.io.File;
005 import java.io.FileWriter;
006 import java.io.IOException;
007 import java.io.OutputStream;
008 import java.io.OutputStreamWriter;
009 import java.io.Writer;
010 import java.util.ArrayList;
011 import java.util.HashMap;
012 import java.util.List;
013 import java.util.Map;
014
015 import biweekly.ICalendar;
016 import biweekly.component.ICalComponent;
017 import biweekly.component.RawComponent;
018 import biweekly.component.marshaller.ComponentLibrary;
019 import biweekly.component.marshaller.ICalComponentMarshaller;
020 import biweekly.component.marshaller.RawComponentMarshaller;
021 import biweekly.io.SkipMeException;
022 import biweekly.io.text.ICalRawWriter.ParameterValueChangedListener;
023 import biweekly.parameter.ICalParameters;
024 import biweekly.property.ICalProperty;
025 import biweekly.property.RawProperty;
026 import biweekly.property.marshaller.ICalPropertyMarshaller;
027 import biweekly.property.marshaller.PropertyLibrary;
028 import biweekly.property.marshaller.RawPropertyMarshaller;
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 * Writes {@link ICalendar} objects to an iCalendar data stream.
058 * </p>
059 *
060 * <pre>
061 * List<ICalendar> icals = ...
062 * Writer writer = ...
063 * ICalWriter icalWriter = new ICalWriter(writer);
064 * for (ICalendar ical : icals){
065 * icalWriter.write(ical);
066 * }
067 * icalWriter.close();
068 * </pre>
069 * @author Michael Angstadt
070 */
071 public class ICalWriter implements Closeable {
072 private final List<String> warnings = new ArrayList<String>();
073 private final Map<Class<? extends ICalProperty>, ICalPropertyMarshaller<? extends ICalProperty>> propertyMarshallers = new HashMap<Class<? extends ICalProperty>, ICalPropertyMarshaller<? extends ICalProperty>>(0);
074 private final Map<Class<? extends ICalComponent>, ICalComponentMarshaller<? extends ICalComponent>> componentMarshallers = new HashMap<Class<? extends ICalComponent>, ICalComponentMarshaller<? extends ICalComponent>>(0);
075 private final ICalRawWriter writer;
076
077 /**
078 * Creates an iCalendar writer that writes to an output stream. Uses the
079 * standard folding scheme and newline sequence.
080 * @param outputStream the output stream to write to
081 */
082 public ICalWriter(OutputStream outputStream) {
083 this(new OutputStreamWriter(outputStream));
084 }
085
086 /**
087 * Creates an iCalendar writer that writes to an output stream. Uses the
088 * standard newline sequence.
089 * @param outputStream the output stream to write to
090 * @param foldingScheme the folding scheme to use or null not to fold at all
091 */
092 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme) throws IOException {
093 this(new OutputStreamWriter(outputStream), foldingScheme);
094 }
095
096 /**
097 * Creates an iCalendar writer that writes to an output stream.
098 * @param outputStream the output stream to write to
099 * @param foldingScheme the folding scheme to use or null not to fold at all
100 * @param newline the newline sequence to use
101 */
102 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme, String newline) throws IOException {
103 this(new OutputStreamWriter(outputStream), foldingScheme, newline);
104 }
105
106 /**
107 * Creates an iCalendar writer that writes to a file. Uses the standard
108 * folding scheme and newline sequence.
109 * @param file the file to write to
110 * @throws IOException if the file cannot be written to
111 */
112 public ICalWriter(File file) throws IOException {
113 this(new FileWriter(file));
114 }
115
116 /**
117 * Creates an iCalendar writer that writes to a file. Uses the standard
118 * newline sequence.
119 * @param file the file to write to
120 * @param foldingScheme the folding scheme to use or null not to fold at all
121 * @throws IOException if the file cannot be written to
122 */
123 public ICalWriter(File file, FoldingScheme foldingScheme) throws IOException {
124 this(new FileWriter(file), foldingScheme);
125 }
126
127 /**
128 * Creates an iCalendar writer that writes to a file.
129 * @param file the file to write to
130 * @param foldingScheme the folding scheme to use or null not to fold at all
131 * @param newline the newline sequence to use
132 * @throws IOException if the file cannot be written to
133 */
134 public ICalWriter(File file, FoldingScheme foldingScheme, String newline) throws IOException {
135 this(new FileWriter(file), foldingScheme, newline);
136 }
137
138 /**
139 * Creates an iCalendar writer that writes to a writer. Uses the standard
140 * folding scheme and newline sequence.
141 * @param writer the writer to the data stream
142 */
143 public ICalWriter(Writer writer) {
144 this(writer, FoldingScheme.DEFAULT);
145 }
146
147 /**
148 * Creates an iCalendar writer that writes to a writer. Uses the standard
149 * newline sequence.
150 * @param writer the writer to the data stream
151 * @param foldingScheme the folding scheme to use or null not to fold at all
152 */
153 public ICalWriter(Writer writer, FoldingScheme foldingScheme) {
154 this(writer, foldingScheme, "\r\n");
155 }
156
157 /**
158 * Creates an iCalendar writer that writes to a writer.
159 * @param writer the writer to the data stream
160 * @param foldingScheme the folding scheme to use or null not to fold at all
161 * @param newline the newline sequence to use
162 */
163 public ICalWriter(Writer writer, FoldingScheme foldingScheme, String newline) {
164 this.writer = new ICalRawWriter(writer, foldingScheme, newline);
165 this.writer.setParameterValueChangedListener(new ParameterValueChangedListener() {
166 public void onParameterValueChanged(String propertyName, String parameterName, String originalValue, String modifiedValue) {
167 warnings.add("Parameter \"" + parameterName + "\" of property \"" + propertyName + "\" contained one or more characters which are not allowed. These characters were removed.");
168 }
169 });
170 }
171
172 /**
173 * <p>
174 * Gets whether the writer will apply circumflex accent encoding on
175 * parameter values (disabled by default). This escaping mechanism allows
176 * for newlines and double quotes to be included in parameter values.
177 * </p>
178 *
179 * <p>
180 * When disabled, the writer will replace newlines with spaces and double
181 * quotes with single quotes.
182 * </p>
183 * @return true if circumflex accent encoding is enabled, false if not
184 * @see ICalRawWriter#isCaretEncodingEnabled()
185 */
186 public boolean isCaretEncodingEnabled() {
187 return writer.isCaretEncodingEnabled();
188 }
189
190 /**
191 * <p>
192 * Sets whether the writer will apply circumflex accent encoding on
193 * parameter values (disabled by default). This escaping mechanism allows
194 * for newlines and double quotes to be included in parameter values.
195 * </p>
196 *
197 * <p>
198 * When disabled, the writer will replace newlines with spaces and double
199 * quotes with single quotes.
200 * </p>
201 * @param enable true to use circumflex accent encoding, false not to
202 * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
203 */
204 public void setCaretEncodingEnabled(boolean enable) {
205 writer.setCaretEncodingEnabled(enable);
206 }
207
208 /**
209 * Gets the newline sequence that is used to separate lines.
210 * @return the newline sequence
211 */
212 public String getNewline() {
213 return writer.getNewline();
214 }
215
216 /**
217 * Gets the rules for how each line is folded.
218 * @return the folding scheme or null if the lines are not folded
219 */
220 public FoldingScheme getFoldingScheme() {
221 return writer.getFoldingScheme();
222 }
223
224 /**
225 * Gets the warnings from the last iCal that was written. This list is reset
226 * every time a new iCal is written.
227 * @return the warnings or empty list if there were no warnings
228 */
229 public List<String> getWarnings() {
230 return new ArrayList<String>(warnings);
231 }
232
233 /**
234 * Registers a marshaller for an experimental property.
235 * @param marshaller the marshaller to register
236 */
237 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
238 propertyMarshallers.put(marshaller.getPropertyClass(), marshaller);
239 }
240
241 /**
242 * Registers a marshaller for an experimental component.
243 * @param marshaller the marshaller to register
244 */
245 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
246 componentMarshallers.put(marshaller.getComponentClass(), marshaller);
247 }
248
249 /**
250 * Writes an iCal to the data stream.
251 * @param ical the iCalendar object to write
252 * @throws IOException
253 */
254 public void write(ICalendar ical) throws IOException {
255 warnings.clear();
256 writeComponent(ical);
257 }
258
259 /**
260 * Writes a component to the data stream.
261 * @param component the component to write
262 * @throws IOException
263 */
264 @SuppressWarnings({ "rawtypes", "unchecked" })
265 private void writeComponent(ICalComponent component) throws IOException {
266 ICalComponentMarshaller m = findComponentMarshaller(component);
267 if (m == null) {
268 warnings.add("No marshaller found for component class \"" + component.getClass().getName() + "\". This component will not be written.");
269 return;
270 }
271
272 writer.writeBeginComponent(m.getComponentName());
273
274 for (Object obj : m.getProperties(component)) {
275 ICalProperty property = (ICalProperty) obj;
276 ICalPropertyMarshaller pm = findPropertyMarshaller(property);
277 if (pm == null) {
278 warnings.add("No marshaller found for property class \"" + property.getClass().getName() + "\". This property will not be written.");
279 continue;
280 }
281
282 //marshal property
283 ICalParameters parameters;
284 String value;
285 try {
286 parameters = pm.prepareParameters(property);
287 value = pm.writeText(property);
288 } catch (SkipMeException e) {
289 if (e.getMessage() == null) {
290 addWarning("Property has requested that it be skipped.", pm.getPropertyName());
291 } else {
292 addWarning("Property has requested that it be skipped: " + e.getMessage(), pm.getPropertyName());
293 }
294 continue;
295 }
296
297 //write property to data stream
298 try {
299 writer.writeProperty(pm.getPropertyName(), parameters, value);
300 } catch (IllegalArgumentException e) {
301 addWarning("Property could not be written: " + e.getMessage(), pm.getPropertyName());
302 continue;
303 }
304 }
305
306 for (Object obj : m.getComponents(component)) {
307 ICalComponent subComponent = (ICalComponent) obj;
308 writeComponent(subComponent);
309 }
310
311 writer.writeEndComponent(m.getComponentName());
312 }
313
314 /**
315 * Finds a component marshaller.
316 * @param component the component being marshalled
317 * @return the component marshaller or null if not found
318 */
319 private ICalComponentMarshaller<? extends ICalComponent> findComponentMarshaller(final ICalComponent component) {
320 ICalComponentMarshaller<? extends ICalComponent> m = componentMarshallers.get(component.getClass());
321 if (m == null) {
322 m = ComponentLibrary.getMarshaller(component.getClass());
323 if (m == null) {
324 if (component instanceof RawComponent) {
325 RawComponent raw = (RawComponent) component;
326 m = new RawComponentMarshaller(raw.getName());
327 }
328 }
329 }
330 return m;
331 }
332
333 /**
334 * Finds a property marshaller.
335 * @param property the property being marshalled
336 * @return the property marshaller or null if not found
337 */
338 private ICalPropertyMarshaller<? extends ICalProperty> findPropertyMarshaller(ICalProperty property) {
339 ICalPropertyMarshaller<? extends ICalProperty> m = propertyMarshallers.get(property.getClass());
340 if (m == null) {
341 m = PropertyLibrary.getMarshaller(property.getClass());
342 if (m == null) {
343 if (property instanceof RawProperty) {
344 RawProperty raw = (RawProperty) property;
345 m = new RawPropertyMarshaller(raw.getName());
346 }
347 }
348 }
349 return m;
350 }
351
352 /**
353 * Closes the underlying {@link Writer} object.
354 */
355 public void close() throws IOException {
356 writer.close();
357 }
358
359 private void addWarning(String message, String propertyName) {
360 warnings.add(propertyName + " property: " + message);
361 }
362 }