001    package biweekly.io.json;
002    
003    import static biweekly.util.IOUtils.utf8Writer;
004    
005    import java.io.Closeable;
006    import java.io.File;
007    import java.io.Flushable;
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 a JSON data stream (jCal).
050     * </p>
051     * <p>
052     * <b>Example:</b>
053     * 
054     * <pre class="brush:java">
055     * List&lt;ICalendar&gt; icals = ... 
056     * OutputStream out = ...
057     * JCalWriter jcalWriter = new JCalWriter(out);
058     * for (ICalendar ical : icals){
059     *   jcalWriter.write(ical);
060     * }
061     * jcalWriter.close();
062     * </pre>
063     * 
064     * </p>
065     * @author Michael Angstadt
066     * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
067     * draft</a>
068     */
069    public class JCalWriter implements Closeable, Flushable {
070            private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
071            private final JCalRawWriter writer;
072    
073            /**
074             * Creates a jCal writer that writes to an output stream.
075             * @param outputStream the output stream to write to
076             */
077            public JCalWriter(OutputStream outputStream) {
078                    this(utf8Writer(outputStream));
079            }
080    
081            /**
082             * Creates a jCal writer that writes to an output stream.
083             * @param outputStream the output stream to write to
084             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
085             * false not to (useful when writing more than one iCalendar object)
086             */
087            public JCalWriter(OutputStream outputStream, boolean wrapInArray) {
088                    this(utf8Writer(outputStream), wrapInArray);
089            }
090    
091            /**
092             * Creates a jCal writer that writes to a file.
093             * @param file the file to write to
094             * @throws IOException if the file cannot be written to
095             */
096            public JCalWriter(File file) throws IOException {
097                    this(utf8Writer(file));
098            }
099    
100            /**
101             * Creates a jCal writer that writes to a file.
102             * @param file the file to write to
103             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
104             * false not to (useful when writing more than one iCalendar object)
105             * @throws IOException if the file cannot be written to
106             */
107            public JCalWriter(File file, boolean wrapInArray) throws IOException {
108                    this(utf8Writer(file), wrapInArray);
109            }
110    
111            /**
112             * Creates a jCal writer that writes to a writer.
113             * @param writer the writer to the data stream
114             */
115            public JCalWriter(Writer writer) {
116                    this(writer, false);
117            }
118    
119            /**
120             * Creates a jCal writer that writes to a writer.
121             * @param writer the writer to the data stream
122             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
123             * false not to (useful when writing more than one iCalendar object)
124             */
125            public JCalWriter(Writer writer, boolean wrapInArray) {
126                    this.writer = new JCalRawWriter(writer, wrapInArray);
127            }
128    
129            /**
130             * <p>
131             * Registers an experimental property marshaller. Can also be used to
132             * override the marshaller of a standard property (such as DTSTART). Calling
133             * this method is the same as calling:
134             * </p>
135             * <p>
136             * {@code getRegistrar().register(marshaller)}.
137             * </p>
138             * @param marshaller the marshaller to register
139             */
140            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
141                    registrar.register(marshaller);
142            }
143    
144            /**
145             * <p>
146             * Registers an experimental component marshaller. Can also be used to
147             * override the marshaller of a standard component (such as VEVENT). Calling
148             * this method is the same as calling:
149             * </p>
150             * <p>
151             * {@code getRegistrar().register(marshaller)}.
152             * </p>
153             * @param marshaller the marshaller to register
154             */
155            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
156                    registrar.register(marshaller);
157            }
158    
159            /**
160             * Gets the object that manages the component/property marshaller objects.
161             * @return the marshaller registrar
162             */
163            public ICalMarshallerRegistrar getRegistrar() {
164                    return registrar;
165            }
166    
167            /**
168             * Sets the object that manages the component/property marshaller objects.
169             * @param registrar the marshaller registrar
170             */
171            public void setRegistrar(ICalMarshallerRegistrar registrar) {
172                    this.registrar = registrar;
173            }
174    
175            /**
176             * Gets whether or not the JSON will be pretty-printed.
177             * @return true if it will be pretty-printed, false if not (defaults to
178             * false)
179             */
180            public boolean isIndent() {
181                    return writer.isIndent();
182            }
183    
184            /**
185             * Sets whether or not to pretty-print the JSON.
186             * @param indent true to pretty-print it, false not to (defaults to false)
187             */
188            public void setIndent(boolean indent) {
189                    writer.setIndent(indent);
190            }
191    
192            /**
193             * Writes an iCalendar object to the data stream.
194             * @param ical the iCalendar object to write
195             * @throws IllegalArgumentException if the marshaller class for a component
196             * or property object cannot be found (only happens when an experimental
197             * property/component marshaller is not registered with the
198             * {@code registerMarshaller} method.)
199             * @throws IOException if there's a problem writing to the data stream
200             */
201            public void write(ICalendar ical) throws IOException {
202                    writeComponent(ical);
203            }
204    
205            /**
206             * Writes a component to the data stream.
207             * @param component the component to write
208             * @throws IllegalArgumentException if the marshaller class for a component
209             * or property object cannot be found (only happens when an experimental
210             * property/component marshaller is not registered with the
211             * {@code registerMarshaller} method.)
212             * @throws IOException if there's a problem writing to the data stream
213             */
214            @SuppressWarnings({ "rawtypes", "unchecked" })
215            private void writeComponent(ICalComponent component) throws IOException {
216                    ICalComponentMarshaller compMarshaller = registrar.getComponentMarshaller(component);
217                    if (compMarshaller == null) {
218                            throw new IllegalArgumentException("No marshaller found for component class \"" + component.getClass().getName() + "\".");
219                    }
220    
221                    writer.writeStartComponent(compMarshaller.getComponentName().toLowerCase());
222    
223                    //write properties
224                    for (Object obj : compMarshaller.getProperties(component)) {
225                            ICalProperty property = (ICalProperty) obj;
226                            ICalPropertyMarshaller propMarshaller = registrar.getPropertyMarshaller(property);
227                            if (propMarshaller == null) {
228                                    throw new IllegalArgumentException("No marshaller found for property class \"" + property.getClass().getName() + "\".");
229                            }
230    
231                            //marshal property
232                            String propertyName = propMarshaller.getPropertyName().toLowerCase();
233                            ICalParameters parameters;
234                            JCalValue value;
235                            try {
236                                    parameters = propMarshaller.prepareParameters(property);
237                                    value = propMarshaller.writeJson(property);
238                            } catch (SkipMeException e) {
239                                    continue;
240                            }
241    
242                            //get the data type
243                            ICalDataType dataType = propMarshaller.dataType(property);
244    
245                            //write property
246                            writer.writeProperty(propertyName, parameters, dataType, value);
247                    }
248    
249                    //write sub-components
250                    for (Object obj : compMarshaller.getComponents(component)) {
251                            ICalComponent subComponent = (ICalComponent) obj;
252                            writeComponent(subComponent);
253                    }
254    
255                    writer.writeEndComponent();
256            }
257    
258            /**
259             * Flushes the stream.
260             * @throws IOException if there's a problem flushing the stream
261             */
262            public void flush() throws IOException {
263                    writer.flush();
264            }
265    
266            /**
267             * Finishes writing the JSON document and closes the underlying
268             * {@link Writer} object.
269             * @throws IOException if there's a problem closing the stream
270             */
271            public void close() throws IOException {
272                    writer.close();
273            }
274    
275            /**
276             * Finishes writing the JSON document so that it is syntactically correct.
277             * No more iCalendar objects can be written once this method is called.
278             * @throws IOException if there's a problem writing to the data stream
279             */
280            public void closeJsonStream() throws IOException {
281                    writer.closeJsonStream();
282            }
283    }