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