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