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&lt;ICalendar&gt; 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    }