001package biweekly.io.json; 002 003import static biweekly.util.StringUtils.NEWLINE; 004 005import java.io.Closeable; 006import java.io.Flushable; 007import java.io.IOException; 008import java.io.Writer; 009import java.util.LinkedList; 010import java.util.List; 011import java.util.Map; 012 013import biweekly.ICalDataType; 014import biweekly.parameter.ICalParameters; 015 016import com.fasterxml.jackson.core.JsonFactory; 017import com.fasterxml.jackson.core.JsonGenerator; 018import com.fasterxml.jackson.core.JsonGenerator.Feature; 019 020/* 021 Copyright (c) 2013-2015, 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/rfc7265">RFC 7265</a> 049 */ 050public class JCalRawWriter implements Closeable, Flushable { 051 private final Writer writer; 052 private final boolean wrapInArray; 053 private final LinkedList<Info> stack = new LinkedList<Info>(); 054 private JsonGenerator generator; 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 (generator == null) { 093 init(); 094 } 095 096 componentEnded = false; 097 098 if (!stack.isEmpty()) { 099 Info parent = stack.getLast(); 100 if (!parent.wroteEndPropertiesArray) { 101 generator.writeEndArray(); 102 parent.wroteEndPropertiesArray = true; 103 } 104 if (!parent.wroteStartSubComponentsArray) { 105 generator.writeStartArray(); 106 parent.wroteStartSubComponentsArray = true; 107 } 108 } 109 110 generator.writeStartArray(); 111 indent(stack.size() * 2); 112 generator.writeString(componentName); 113 generator.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 generator.writeEndArray(); 132 } 133 if (!cur.wroteStartSubComponentsArray) { 134 generator.writeStartArray(); 135 } 136 137 generator.writeEndArray(); //end sub-components array 138 generator.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 generator.writeStartArray(); 177 indent(stack.size() * 2); 178 179 //write the property name 180 generator.writeString(propertyName); 181 182 //write parameters 183 generator.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 generator.writeStringField(name, values.get(0)); 193 } else { 194 generator.writeArrayFieldStart(name); 195 for (String paramValue : values) { 196 generator.writeString(paramValue); 197 } 198 generator.writeEndArray(); 199 } 200 } 201 generator.writeEndObject(); 202 203 //write data type 204 generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase()); 205 206 //write value 207 for (JsonValue jsonValue : value.getValues()) { 208 writeValue(jsonValue); 209 } 210 211 generator.writeEndArray(); 212 } 213 214 private void writeValue(JsonValue jsonValue) throws IOException { 215 if (jsonValue.isNull()) { 216 generator.writeNull(); 217 return; 218 } 219 220 Object val = jsonValue.getValue(); 221 if (val != null) { 222 if (val instanceof Byte) { 223 generator.writeNumber((Byte) val); 224 } else if (val instanceof Short) { 225 generator.writeNumber((Short) val); 226 } else if (val instanceof Integer) { 227 generator.writeNumber((Integer) val); 228 } else if (val instanceof Long) { 229 generator.writeNumber((Long) val); 230 } else if (val instanceof Float) { 231 generator.writeNumber((Float) val); 232 } else if (val instanceof Double) { 233 generator.writeNumber((Double) val); 234 } else if (val instanceof Boolean) { 235 generator.writeBoolean((Boolean) val); 236 } else { 237 generator.writeString(val.toString()); 238 } 239 return; 240 } 241 242 List<JsonValue> array = jsonValue.getArray(); 243 if (array != null) { 244 generator.writeStartArray(); 245 for (JsonValue element : array) { 246 writeValue(element); 247 } 248 generator.writeEndArray(); 249 return; 250 } 251 252 Map<String, JsonValue> object = jsonValue.getObject(); 253 if (object != null) { 254 generator.writeStartObject(); 255 for (Map.Entry<String, JsonValue> entry : object.entrySet()) { 256 generator.writeFieldName(entry.getKey()); 257 writeValue(entry.getValue()); 258 } 259 generator.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 return; 273 } 274 275 generator.writeRaw(NEWLINE); 276 for (int i = 0; i < spaces; i++) { 277 generator.writeRaw(' '); 278 } 279 } 280 281 /** 282 * Flushes the JSON stream. 283 * @throws IOException if there's a problem flushing the stream 284 */ 285 public void flush() throws IOException { 286 if (generator == null) { 287 return; 288 } 289 290 generator.flush(); 291 } 292 293 /** 294 * Finishes writing the JSON document so that it is syntactically correct. 295 * No more data can be written once this method is called. 296 * @throws IOException if there's a problem closing the stream 297 */ 298 public void closeJsonStream() throws IOException { 299 if (generator == null) { 300 return; 301 } 302 303 while (!stack.isEmpty()) { 304 writeEndComponent(); 305 } 306 307 if (wrapInArray) { 308 indent(0); 309 generator.writeEndArray(); 310 } 311 312 generator.close(); 313 } 314 315 /** 316 * Finishes writing the JSON document and closes the underlying 317 * {@link Writer}. 318 * @throws IOException if there's a problem closing the stream 319 */ 320 public void close() throws IOException { 321 if (generator == null) { 322 return; 323 } 324 325 closeJsonStream(); 326 writer.close(); 327 } 328 329 private void init() throws IOException { 330 JsonFactory factory = new JsonFactory(); 331 factory.configure(Feature.AUTO_CLOSE_TARGET, false); 332 generator = factory.createGenerator(writer); 333 334 if (wrapInArray) { 335 generator.writeStartArray(); 336 indent(0); 337 } 338 } 339 340 private static class Info { 341 public boolean wroteEndPropertiesArray = false; 342 public boolean wroteStartSubComponentsArray = false; 343 } 344}