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.IOException;
008    import java.io.OutputStream;
009    import java.io.Writer;
010    
011    import biweekly.ICalDataType;
012    import biweekly.ICalendar;
013    import biweekly.component.ICalComponent;
014    import biweekly.component.marshaller.ICalComponentMarshaller;
015    import biweekly.io.ICalMarshallerRegistrar;
016    import biweekly.io.SkipMeException;
017    import biweekly.parameter.ICalParameters;
018    import biweekly.property.ICalProperty;
019    import biweekly.property.marshaller.ICalPropertyMarshaller;
020    
021    /*
022     Copyright (c) 2013, Michael Angstadt
023     All rights reserved.
024    
025     Redistribution and use in source and binary forms, with or without
026     modification, are permitted provided that the following conditions are met: 
027    
028     1. Redistributions of source code must retain the above copyright notice, this
029     list of conditions and the following disclaimer. 
030     2. Redistributions in binary form must reproduce the above copyright notice,
031     this list of conditions and the following disclaimer in the documentation
032     and/or other materials provided with the distribution. 
033    
034     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
035     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
036     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
037     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
038     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
039     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
040     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
042     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
043     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
044     */
045    
046    /**
047     * <p>
048     * Writes {@link ICalendar} objects to a JSON data stream (jCal).
049     * </p>
050     * <p>
051     * <b>Example:</b>
052     * 
053     * <pre class="brush:java">
054     * List&lt;ICalendar&gt; icals = ... 
055     * OutputStream out = ...
056     * JCalWriter jcalWriter = new JCalWriter(out);
057     * for (ICalendar ical : icals){
058     *   jcalWriter.write(ical);
059     * }
060     * jcalWriter.close();
061     * </pre>
062     * 
063     * </p>
064     * @author Michael Angstadt
065     * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
066     * draft</a>
067     */
068    public class JCalWriter implements Closeable {
069            private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
070            private final JCalRawWriter writer;
071    
072            /**
073             * Creates a jCal writer that writes to an output stream.
074             * @param outputStream the output stream to write to
075             */
076            public JCalWriter(OutputStream outputStream) {
077                    this(utf8Writer(outputStream));
078            }
079    
080            /**
081             * Creates a jCal writer that writes to an output stream.
082             * @param outputStream the output stream to write to
083             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
084             * false not to (useful when writing more than one iCalendar object)
085             */
086            public JCalWriter(OutputStream outputStream, boolean wrapInArray) {
087                    this(utf8Writer(outputStream), wrapInArray);
088            }
089    
090            /**
091             * Creates a jCal writer that writes to a file.
092             * @param file the file to write to
093             * @throws IOException if the file cannot be written to
094             */
095            public JCalWriter(File file) throws IOException {
096                    this(utf8Writer(file));
097            }
098    
099            /**
100             * Creates a jCal writer that writes to a file.
101             * @param file the file to write to
102             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
103             * false not to (useful when writing more than one iCalendar object)
104             * @throws IOException if the file cannot be written to
105             */
106            public JCalWriter(File file, boolean wrapInArray) throws IOException {
107                    this(utf8Writer(file), wrapInArray);
108            }
109    
110            /**
111             * Creates a jCal writer that writes to a writer.
112             * @param writer the writer to the data stream
113             */
114            public JCalWriter(Writer writer) {
115                    this(writer, false);
116            }
117    
118            /**
119             * Creates a jCal writer that writes to a writer.
120             * @param writer the writer to the data stream
121             * @param wrapInArray true to wrap all iCalendar objects in a parent array,
122             * false not to (useful when writing more than one iCalendar object)
123             */
124            public JCalWriter(Writer writer, boolean wrapInArray) {
125                    this.writer = new JCalRawWriter(writer, wrapInArray);
126            }
127    
128            /**
129             * <p>
130             * Registers an experimental property marshaller. Can also be used to
131             * override the marshaller of a standard property (such as DTSTART). Calling
132             * this method is the same as calling:
133             * </p>
134             * <p>
135             * {@code getRegistrar().register(marshaller)}.
136             * </p>
137             * @param marshaller the marshaller to register
138             */
139            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
140                    registrar.register(marshaller);
141            }
142    
143            /**
144             * <p>
145             * Registers an experimental component marshaller. Can also be used to
146             * override the marshaller of a standard component (such as VEVENT). Calling
147             * this method is the same as calling:
148             * </p>
149             * <p>
150             * {@code getRegistrar().register(marshaller)}.
151             * </p>
152             * @param marshaller the marshaller to register
153             */
154            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
155                    registrar.register(marshaller);
156            }
157    
158            /**
159             * Gets the object that manages the component/property marshaller objects.
160             * @return the marshaller registrar
161             */
162            public ICalMarshallerRegistrar getRegistrar() {
163                    return registrar;
164            }
165    
166            /**
167             * Sets the object that manages the component/property marshaller objects.
168             * @param registrar the marshaller registrar
169             */
170            public void setRegistrar(ICalMarshallerRegistrar registrar) {
171                    this.registrar = registrar;
172            }
173    
174            /**
175             * Gets whether or not the JSON will be pretty-printed.
176             * @return true if it will be pretty-printed, false if not (defaults to
177             * false)
178             */
179            public boolean isIndent() {
180                    return writer.isIndent();
181            }
182    
183            /**
184             * Sets whether or not to pretty-print the JSON.
185             * @param indent true to pretty-print it, false not to (defaults to false)
186             */
187            public void setIndent(boolean indent) {
188                    writer.setIndent(indent);
189            }
190    
191            /**
192             * Writes an iCalendar object to the data stream.
193             * @param ical the iCalendar object to write
194             * @throws IllegalArgumentException if the marshaller class for a component
195             * or property object cannot be found (only happens when an experimental
196             * property/component marshaller is not registered with the
197             * {@code registerMarshaller} method.)
198             * @throws IOException if there's a problem writing to the data stream
199             */
200            public void write(ICalendar ical) throws IOException {
201                    writeComponent(ical);
202            }
203    
204            /**
205             * Writes a component to the data stream.
206             * @param component the component to write
207             * @throws IllegalArgumentException if the marshaller class for a component
208             * or property object cannot be found (only happens when an experimental
209             * property/component marshaller is not registered with the
210             * {@code registerMarshaller} method.)
211             * @throws IOException if there's a problem writing to the data stream
212             */
213            @SuppressWarnings({ "rawtypes", "unchecked" })
214            private void writeComponent(ICalComponent component) throws IOException {
215                    ICalComponentMarshaller compMarshaller = registrar.getComponentMarshaller(component);
216                    if (compMarshaller == null) {
217                            throw new IllegalArgumentException("No marshaller found for component class \"" + component.getClass().getName() + "\".");
218                    }
219    
220                    writer.writeStartComponent(compMarshaller.getComponentName().toLowerCase());
221    
222                    //write properties
223                    for (Object obj : compMarshaller.getProperties(component)) {
224                            ICalProperty property = (ICalProperty) obj;
225                            ICalPropertyMarshaller propMarshaller = registrar.getPropertyMarshaller(property);
226                            if (propMarshaller == null) {
227                                    throw new IllegalArgumentException("No marshaller found for property class \"" + property.getClass().getName() + "\".");
228                            }
229    
230                            //marshal property
231                            String propertyName = propMarshaller.getPropertyName().toLowerCase();
232                            ICalParameters parameters;
233                            JCalValue value;
234                            try {
235                                    parameters = propMarshaller.prepareParameters(property);
236                                    value = propMarshaller.writeJson(property);
237                            } catch (SkipMeException e) {
238                                    continue;
239                            }
240    
241                            //get the data type
242                            ICalDataType dataType = propMarshaller.dataType(property);
243    
244                            //write property
245                            writer.writeProperty(propertyName, parameters, dataType, value);
246                    }
247    
248                    //write sub-components
249                    for (Object obj : compMarshaller.getComponents(component)) {
250                            ICalComponent subComponent = (ICalComponent) obj;
251                            writeComponent(subComponent);
252                    }
253    
254                    writer.writeEndComponent();
255            }
256    
257            /**
258             * Finishes writing the JSON document and closes the underlying
259             * {@link Writer}.
260             * @throws IOException if there's a problem closing the stream
261             */
262            public void close() throws IOException {
263                    writer.close();
264            }
265    
266            /**
267             * Finishes writing the JSON document so that it is syntactically correct.
268             * No more iCalendar objects can be written once this method is called.
269             * @throws IOException if there's a problem writing to the data stream
270             */
271            public void closeJsonStream() throws IOException {
272                    writer.closeJsonStream();
273            }
274    }