001 package biweekly.io.json;
002
003 import static biweekly.util.StringUtils.NEWLINE;
004
005 import java.io.Closeable;
006 import java.io.Flushable;
007 import java.io.IOException;
008 import java.io.Writer;
009 import java.util.LinkedList;
010 import java.util.List;
011 import java.util.Map;
012
013 import biweekly.ICalDataType;
014 import biweekly.parameter.ICalParameters;
015
016 import com.fasterxml.jackson.core.JsonFactory;
017 import com.fasterxml.jackson.core.JsonGenerator;
018 import com.fasterxml.jackson.core.JsonGenerator.Feature;
019
020 /*
021 Copyright (c) 2013, Michael Angstadt
022 All rights reserved.
023
024 Redistribution and use in source and binary forms, with or without
025 modification, are permitted provided that the following conditions are met:
026
027 1. Redistributions of source code must retain the above copyright notice, this
028 list of conditions and the following disclaimer.
029 2. Redistributions in binary form must reproduce the above copyright notice,
030 this list of conditions and the following disclaimer in the documentation
031 and/or other materials provided with the distribution.
032
033 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
034 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
035 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
037 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
039 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
040 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
041 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
042 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043 */
044
045 /**
046 * Writes data to an iCalendar JSON data stream (jCal).
047 * @author Michael Angstadt
048 * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
049 * draft</a>
050 */
051 public class JCalRawWriter implements Closeable, Flushable {
052 private final Writer writer;
053 private final boolean wrapInArray;
054 private final LinkedList<Info> stack = new LinkedList<Info>();
055 private JsonGenerator jg;
056 private boolean indent = false;
057 private boolean componentEnded = false;
058
059 /**
060 * Creates a new raw writer.
061 * @param writer the writer to the data stream
062 * @param wrapInArray true to wrap everything in an array, false not to
063 * (useful when writing more than one iCalendar object)
064 */
065 public JCalRawWriter(Writer writer, boolean wrapInArray) {
066 this.writer = writer;
067 this.wrapInArray = wrapInArray;
068 }
069
070 /**
071 * Gets whether or not the JSON will be pretty-printed.
072 * @return true if it will be pretty-printed, false if not (defaults to
073 * false)
074 */
075 public boolean isIndent() {
076 return indent;
077 }
078
079 /**
080 * Sets whether or not to pretty-print the JSON.
081 * @param indent true to pretty-print it, false not to (defaults to false)
082 */
083 public void setIndent(boolean indent) {
084 this.indent = indent;
085 }
086
087 /**
088 * Writes the beginning of a new component array.
089 * @param componentName the component name (e.g. "vevent")
090 * @throws IOException if there's an I/O problem
091 */
092 public void writeStartComponent(String componentName) throws IOException {
093 if (jg == null) {
094 init();
095 }
096
097 componentEnded = false;
098
099 if (!stack.isEmpty()) {
100 Info parent = stack.getLast();
101 if (!parent.wroteEndPropertiesArray) {
102 jg.writeEndArray();
103 parent.wroteEndPropertiesArray = true;
104 }
105 if (!parent.wroteStartSubComponentsArray) {
106 jg.writeStartArray();
107 parent.wroteStartSubComponentsArray = true;
108 }
109 }
110
111 jg.writeStartArray();
112 indent(stack.size() * 2);
113 jg.writeString(componentName);
114 jg.writeStartArray(); //start properties array
115
116 stack.add(new Info());
117 }
118
119 /**
120 * Closes the current component array.
121 * @throws IllegalStateException if there are no open components (
122 * {@link #writeStartComponent(String)} must be called first)
123 * @throws IOException if there's an I/O problem
124 */
125 public void writeEndComponent() throws IOException {
126 if (stack.isEmpty()) {
127 throw new IllegalStateException("Call \"writeStartComponent\" first.");
128 }
129 Info cur = stack.removeLast();
130
131 if (!cur.wroteEndPropertiesArray) {
132 jg.writeEndArray();
133 }
134 if (!cur.wroteStartSubComponentsArray) {
135 jg.writeStartArray();
136 }
137
138 jg.writeEndArray(); //end sub-components array
139 jg.writeEndArray(); //end the array of this component
140
141 componentEnded = true;
142 }
143
144 /**
145 * Writes a property to the current component.
146 * @param propertyName the property name (e.g. "version")
147 * @param dataType the property's data type (e.g. "text")
148 * @param value the property value
149 * @throws IllegalStateException if there are no open components (
150 * {@link #writeStartComponent(String)} must be called first) or if the last
151 * method called was {@link #writeEndComponent()}.
152 * @throws IOException if there's an I/O problem
153 */
154 public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException {
155 writeProperty(propertyName, new ICalParameters(), dataType, value);
156 }
157
158 /**
159 * Writes a property to the current component.
160 * @param propertyName the property name (e.g. "version")
161 * @param parameters the parameters
162 * @param dataType the property's data type (e.g. "text")
163 * @param value the property value
164 * @throws IllegalStateException if there are no open components (
165 * {@link #writeStartComponent(String)} must be called first) or if the last
166 * method called was {@link #writeEndComponent()}.
167 * @throws IOException if there's an I/O problem
168 */
169 public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException {
170 if (stack.isEmpty()) {
171 throw new IllegalStateException("Call \"writeStartComponent\" first.");
172 }
173 if (componentEnded) {
174 throw new IllegalStateException("Cannot write a property after calling \"writeEndComponent\".");
175 }
176
177 jg.writeStartArray();
178 indent(stack.size() * 2);
179
180 //write the property name
181 jg.writeString(propertyName);
182
183 //write parameters
184 jg.writeStartObject();
185 for (Map.Entry<String, List<String>> entry : parameters) {
186 String name = entry.getKey().toLowerCase();
187 List<String> values = entry.getValue();
188 if (values.isEmpty()) {
189 continue;
190 }
191
192 if (values.size() == 1) {
193 jg.writeStringField(name, values.get(0));
194 } else {
195 jg.writeArrayFieldStart(name);
196 for (String paramValue : values) {
197 jg.writeString(paramValue);
198 }
199 jg.writeEndArray();
200 }
201 }
202 jg.writeEndObject();
203
204 //write data type
205 jg.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase());
206
207 //write value
208 for (JsonValue jsonValue : value.getValues()) {
209 writeValue(jsonValue);
210 }
211
212 jg.writeEndArray();
213 }
214
215 private void writeValue(JsonValue jsonValue) throws IOException {
216 if (jsonValue.isNull()) {
217 jg.writeNull();
218 return;
219 }
220
221 Object val = jsonValue.getValue();
222 if (val != null) {
223 if (val instanceof Byte) {
224 jg.writeNumber((Byte) val);
225 } else if (val instanceof Short) {
226 jg.writeNumber((Short) val);
227 } else if (val instanceof Integer) {
228 jg.writeNumber((Integer) val);
229 } else if (val instanceof Long) {
230 jg.writeNumber((Long) val);
231 } else if (val instanceof Float) {
232 jg.writeNumber((Float) val);
233 } else if (val instanceof Double) {
234 jg.writeNumber((Double) val);
235 } else if (val instanceof Boolean) {
236 jg.writeBoolean((Boolean) val);
237 } else {
238 jg.writeString(val.toString());
239 }
240 return;
241 }
242
243 List<JsonValue> array = jsonValue.getArray();
244 if (array != null) {
245 jg.writeStartArray();
246 for (JsonValue element : array) {
247 writeValue(element);
248 }
249 jg.writeEndArray();
250 return;
251 }
252
253 Map<String, JsonValue> object = jsonValue.getObject();
254 if (object != null) {
255 jg.writeStartObject();
256 for (Map.Entry<String, JsonValue> entry : object.entrySet()) {
257 jg.writeFieldName(entry.getKey());
258 writeValue(entry.getValue());
259 }
260 jg.writeEndObject();
261 return;
262 }
263 }
264
265 /**
266 * Checks to see if pretty-printing is enabled, and adds indentation
267 * whitespace if it is.
268 * @param spaces the number of spaces to indent with
269 * @throws IOException
270 */
271 private void indent(int spaces) throws IOException {
272 if (indent) {
273 jg.writeRaw(NEWLINE);
274 for (int i = 0; i < spaces; i++) {
275 jg.writeRaw(' ');
276 }
277 }
278 }
279
280 /**
281 * Flushes the JSON stream.
282 * @throws IOException if there's a problem flushing the stream
283 */
284 public void flush() throws IOException {
285 if (jg == null) {
286 return;
287 }
288
289 jg.flush();
290 }
291
292 /**
293 * Finishes writing the JSON document so that it is syntactically correct.
294 * No more data can be written once this method is called.
295 * @throws IOException if there's a problem closing the stream
296 */
297 public void closeJsonStream() throws IOException {
298 if (jg == null) {
299 return;
300 }
301
302 while (!stack.isEmpty()) {
303 writeEndComponent();
304 }
305
306 if (wrapInArray) {
307 indent(0);
308 jg.writeEndArray();
309 }
310
311 jg.close();
312 }
313
314 /**
315 * Finishes writing the JSON document and closes the underlying
316 * {@link Writer}.
317 * @throws IOException if there's a problem closing the stream
318 */
319 public void close() throws IOException {
320 if (jg == null) {
321 return;
322 }
323
324 closeJsonStream();
325 writer.close();
326 }
327
328 private void init() throws IOException {
329 JsonFactory factory = new JsonFactory();
330 factory.configure(Feature.AUTO_CLOSE_TARGET, false);
331 jg = factory.createJsonGenerator(writer);
332
333 if (wrapInArray) {
334 jg.writeStartArray();
335 indent(0);
336 }
337 }
338
339 private static class Info {
340 public boolean wroteEndPropertiesArray = false;
341 public boolean wroteStartSubComponentsArray = false;
342 }
343 }