001 package biweekly.io.text;
002
003 import static biweekly.util.IOUtils.utf8Writer;
004
005 import java.io.Closeable;
006 import java.io.File;
007 import java.io.FileNotFoundException;
008 import java.io.Flushable;
009 import java.io.IOException;
010 import java.io.OutputStream;
011 import java.io.Writer;
012
013 import biweekly.ICalDataType;
014 import biweekly.ICalendar;
015 import biweekly.component.ICalComponent;
016 import biweekly.component.marshaller.ICalComponentMarshaller;
017 import biweekly.io.ICalMarshallerRegistrar;
018 import biweekly.io.SkipMeException;
019 import biweekly.parameter.ICalParameters;
020 import biweekly.property.ICalProperty;
021 import biweekly.property.marshaller.ICalPropertyMarshaller;
022
023 /*
024 Copyright (c) 2013, Michael Angstadt
025 All rights reserved.
026
027 Redistribution and use in source and binary forms, with or without
028 modification, are permitted provided that the following conditions are met:
029
030 1. Redistributions of source code must retain the above copyright notice, this
031 list of conditions and the following disclaimer.
032 2. Redistributions in binary form must reproduce the above copyright notice,
033 this list of conditions and the following disclaimer in the documentation
034 and/or other materials provided with the distribution.
035
036 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
037 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
038 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
039 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
040 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
041 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
042 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
044 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
045 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
046 */
047
048 /**
049 * <p>
050 * Writes {@link ICalendar} objects to an iCalendar data stream.
051 * </p>
052 * <p>
053 * <b>Example:</b>
054 *
055 * <pre class="brush:java">
056 * List<ICalendar> icals = ...
057 * OutputStream out = ...
058 * ICalWriter icalWriter = new ICalWriter(out);
059 * for (ICalendar ical : icals){
060 * icalWriter.write(ical);
061 * }
062 * icalWriter.close();
063 * </pre>
064 *
065 * </p>
066 * @author Michael Angstadt
067 * @rfc 5545
068 */
069 public class ICalWriter implements Closeable, Flushable {
070 private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
071 private final ICalRawWriter writer;
072
073 /**
074 * Creates an iCalendar writer that writes to an output stream. Uses the
075 * standard folding scheme and newline sequence.
076 * @param outputStream the output stream to write to
077 */
078 public ICalWriter(OutputStream outputStream) {
079 this(utf8Writer(outputStream));
080 }
081
082 /**
083 * Creates an iCalendar writer that writes to an output stream. Uses the
084 * standard newline sequence.
085 * @param outputStream the output stream to write to
086 * @param foldingScheme the folding scheme to use or null not to fold at all
087 */
088 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme) {
089 this(utf8Writer(outputStream), foldingScheme);
090 }
091
092 /**
093 * Creates an iCalendar writer that writes to an output stream.
094 * @param outputStream the output stream to write to
095 * @param foldingScheme the folding scheme to use or null not to fold at all
096 * @param newline the newline sequence to use
097 */
098 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme, String newline) {
099 this(utf8Writer(outputStream), foldingScheme, newline);
100 }
101
102 /**
103 * Creates an iCalendar writer that writes to a file. Uses the standard
104 * folding scheme and newline sequence.
105 * @param file the file to write to
106 * @throws FileNotFoundException if the file cannot be written to
107 */
108 public ICalWriter(File file) throws FileNotFoundException {
109 this(utf8Writer(file));
110 }
111
112 /**
113 * Creates an iCalendar writer that writes to a file. Uses the standard
114 * folding scheme and newline sequence.
115 * @param file the file to write to
116 * @param append true to append to the end of the file, false to overwrite
117 * it
118 * @throws FileNotFoundException if the file cannot be written to
119 */
120 public ICalWriter(File file, boolean append) throws FileNotFoundException {
121 this(utf8Writer(file, append));
122 }
123
124 /**
125 * Creates an iCalendar writer that writes to a file. Uses the standard
126 * newline sequence.
127 * @param file the file to write to
128 * @param append true to append to the end of the file, false to overwrite
129 * it
130 * @param foldingScheme the folding scheme to use or null not to fold at all
131 * @throws FileNotFoundException if the file cannot be written to
132 */
133 public ICalWriter(File file, boolean append, FoldingScheme foldingScheme) throws FileNotFoundException {
134 this(utf8Writer(file, append), foldingScheme);
135 }
136
137 /**
138 * Creates an iCalendar writer that writes to a file.
139 * @param file the file to write to
140 * @param append true to append to the end of the file, false to overwrite
141 * it
142 * @param foldingScheme the folding scheme to use or null not to fold at all
143 * @param newline the newline sequence to use
144 * @throws FileNotFoundException if the file cannot be written to
145 */
146 public ICalWriter(File file, boolean append, FoldingScheme foldingScheme, String newline) throws FileNotFoundException {
147 this(utf8Writer(file, append), foldingScheme, newline);
148 }
149
150 /**
151 * Creates an iCalendar writer that writes to a writer. Uses the standard
152 * folding scheme and newline sequence.
153 * @param writer the writer to the data stream
154 */
155 public ICalWriter(Writer writer) {
156 this(writer, FoldingScheme.DEFAULT);
157 }
158
159 /**
160 * Creates an iCalendar writer that writes to a writer. Uses the standard
161 * newline sequence.
162 * @param writer the writer to the data stream
163 * @param foldingScheme the folding scheme to use or null not to fold at all
164 */
165 public ICalWriter(Writer writer, FoldingScheme foldingScheme) {
166 this(writer, foldingScheme, "\r\n");
167 }
168
169 /**
170 * Creates an iCalendar writer that writes to a writer.
171 * @param writer the writer to the data stream
172 * @param foldingScheme the folding scheme to use or null not to fold at all
173 * @param newline the newline sequence to use
174 */
175 public ICalWriter(Writer writer, FoldingScheme foldingScheme, String newline) {
176 this.writer = new ICalRawWriter(writer, foldingScheme, newline);
177 }
178
179 /**
180 * <p>
181 * Gets whether the writer will apply circumflex accent encoding on
182 * parameter values (disabled by default). This escaping mechanism allows
183 * for newlines and double quotes to be included in parameter values.
184 * </p>
185 *
186 * <p>
187 * When disabled, the writer will replace newlines with spaces and double
188 * quotes with single quotes.
189 * </p>
190 * @return true if circumflex accent encoding is enabled, false if not
191 * @see ICalRawWriter#isCaretEncodingEnabled()
192 */
193 public boolean isCaretEncodingEnabled() {
194 return writer.isCaretEncodingEnabled();
195 }
196
197 /**
198 * <p>
199 * Sets whether the writer will apply circumflex accent encoding on
200 * parameter values (disabled by default). This escaping mechanism allows
201 * for newlines and double quotes to be included in parameter values.
202 * </p>
203 *
204 * <p>
205 * When disabled, the writer will replace newlines with spaces and double
206 * quotes with single quotes.
207 * </p>
208 * @param enable true to use circumflex accent encoding, false not to
209 * @see ICalRawWriter#setCaretEncodingEnabled(boolean)
210 */
211 public void setCaretEncodingEnabled(boolean enable) {
212 writer.setCaretEncodingEnabled(enable);
213 }
214
215 /**
216 * Gets the newline sequence that is used to separate lines.
217 * @return the newline sequence
218 */
219 public String getNewline() {
220 return writer.getNewline();
221 }
222
223 /**
224 * Gets the rules for how each line is folded.
225 * @return the folding scheme or null if the lines are not folded
226 */
227 public FoldingScheme getFoldingScheme() {
228 return writer.getFoldingScheme();
229 }
230
231 /**
232 * <p>
233 * Registers an experimental property marshaller. Can also be used to
234 * override the marshaller of a standard property (such as DTSTART). Calling
235 * this method is the same as calling:
236 * </p>
237 * <p>
238 * {@code getRegistrar().register(marshaller)}.
239 * </p>
240 * @param marshaller the marshaller to register
241 */
242 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
243 registrar.register(marshaller);
244 }
245
246 /**
247 * <p>
248 * Registers an experimental component marshaller. Can also be used to
249 * override the marshaller of a standard component (such as VEVENT). Calling
250 * this method is the same as calling:
251 * </p>
252 * <p>
253 * {@code getRegistrar().register(marshaller)}.
254 * </p>
255 * @param marshaller the marshaller to register
256 */
257 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
258 registrar.register(marshaller);
259 }
260
261 /**
262 * Gets the object that manages the component/property marshaller objects.
263 * @return the marshaller registrar
264 */
265 public ICalMarshallerRegistrar getRegistrar() {
266 return registrar;
267 }
268
269 /**
270 * Sets the object that manages the component/property marshaller objects.
271 * @param registrar the marshaller registrar
272 */
273 public void setRegistrar(ICalMarshallerRegistrar registrar) {
274 this.registrar = registrar;
275 }
276
277 /**
278 * Writes an iCalendar object to the data stream.
279 * @param ical the iCalendar object to write
280 * @throws IllegalArgumentException if the marshaller class for a component
281 * or property object cannot be found (only happens when an experimental
282 * property/component marshaller is not registered with the
283 * {@code registerMarshaller} method.)
284 * @throws IOException if there's a problem writing to the data stream
285 */
286 public void write(ICalendar ical) throws IOException {
287 writeComponent(ical);
288 }
289
290 /**
291 * Writes a component to the data stream.
292 * @param component the component to write
293 * @throws IOException if there's a problem writing to the data stream
294 */
295 @SuppressWarnings({ "rawtypes", "unchecked" })
296 private void writeComponent(ICalComponent component) throws IOException {
297 ICalComponentMarshaller m = registrar.getComponentMarshaller(component);
298 if (m == null) {
299 throw new IllegalArgumentException("No marshaller found for component class \"" + component.getClass().getName() + "\".");
300 }
301
302 writer.writeBeginComponent(m.getComponentName());
303
304 for (Object obj : m.getProperties(component)) {
305 ICalProperty property = (ICalProperty) obj;
306 ICalPropertyMarshaller pm = registrar.getPropertyMarshaller(property);
307 if (pm == null) {
308 throw new IllegalArgumentException("No marshaller found for property class \"" + property.getClass().getName() + "\".");
309 }
310
311 //marshal property
312 ICalParameters parameters;
313 String value;
314 try {
315 parameters = pm.prepareParameters(property);
316 value = pm.writeText(property);
317 } catch (SkipMeException e) {
318 continue;
319 }
320
321 //set the data type
322 ICalDataType dataType = pm.dataType(property);
323 if (dataType != null && dataType != pm.getDefaultDataType()) {
324 //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type
325 parameters.setValue(dataType);
326 }
327
328 //write property to data stream
329 writer.writeProperty(pm.getPropertyName(), parameters, value);
330 }
331
332 for (Object obj : m.getComponents(component)) {
333 ICalComponent subComponent = (ICalComponent) obj;
334 writeComponent(subComponent);
335 }
336
337 writer.writeEndComponent(m.getComponentName());
338 }
339
340 /**
341 * Flushes the stream.
342 * @throws IOException if there's a problem flushing the stream
343 */
344 public void flush() throws IOException {
345 writer.flush();
346 }
347
348 /**
349 * Closes the underlying {@link Writer} object.
350 */
351 public void close() throws IOException {
352 writer.close();
353 }
354 }