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 * @param writer the writer to wrap 060 * @param wrapInArray true to wrap everything in an array, false not to 061 * (useful when writing more than one iCalendar object) 062 */ 063 public JCalRawWriter(Writer writer, boolean wrapInArray) { 064 this.writer = writer; 065 this.wrapInArray = wrapInArray; 066 } 067 068 /** 069 * Gets whether or not the JSON will be pretty-printed. 070 * @return true if it will be pretty-printed, false if not (defaults to 071 * false) 072 */ 073 public boolean isIndent() { 074 return indent; 075 } 076 077 /** 078 * Sets whether or not to pretty-print the JSON. 079 * @param indent true to pretty-print it, false not to (defaults to false) 080 */ 081 public void setIndent(boolean indent) { 082 this.indent = indent; 083 } 084 085 /** 086 * Writes the beginning of a new component array. 087 * @param componentName the component name (e.g. "vevent") 088 * @throws IOException if there's an I/O problem 089 */ 090 public void writeStartComponent(String componentName) throws IOException { 091 if (generator == null) { 092 init(); 093 } 094 095 componentEnded = false; 096 097 if (!stack.isEmpty()) { 098 Info parent = stack.getLast(); 099 if (!parent.wroteEndPropertiesArray) { 100 generator.writeEndArray(); 101 parent.wroteEndPropertiesArray = true; 102 } 103 if (!parent.wroteStartSubComponentsArray) { 104 generator.writeStartArray(); 105 parent.wroteStartSubComponentsArray = true; 106 } 107 } 108 109 generator.writeStartArray(); 110 indent(stack.size() * 2); 111 generator.writeString(componentName); 112 generator.writeStartArray(); //start properties array 113 114 stack.add(new Info()); 115 } 116 117 /** 118 * Closes the current component array. 119 * @throws IllegalStateException if there are no open components ( 120 * {@link #writeStartComponent(String)} must be called first) 121 * @throws IOException if there's an I/O problem 122 */ 123 public void writeEndComponent() throws IOException { 124 if (stack.isEmpty()) { 125 throw new IllegalStateException("Call \"writeStartComponent\" first."); 126 } 127 Info cur = stack.removeLast(); 128 129 if (!cur.wroteEndPropertiesArray) { 130 generator.writeEndArray(); 131 } 132 if (!cur.wroteStartSubComponentsArray) { 133 generator.writeStartArray(); 134 } 135 136 generator.writeEndArray(); //end sub-components array 137 generator.writeEndArray(); //end the array of this component 138 139 componentEnded = true; 140 } 141 142 /** 143 * Writes a property to the current component. 144 * @param propertyName the property name (e.g. "version") 145 * @param dataType the property's data type (e.g. "text") 146 * @param value the property value 147 * @throws IllegalStateException if there are no open components ( 148 * {@link #writeStartComponent(String)} must be called first) or if the last 149 * method called was {@link #writeEndComponent()}. 150 * @throws IOException if there's an I/O problem 151 */ 152 public void writeProperty(String propertyName, ICalDataType dataType, JCalValue value) throws IOException { 153 writeProperty(propertyName, new ICalParameters(), dataType, value); 154 } 155 156 /** 157 * Writes a property to the current component. 158 * @param propertyName the property name (e.g. "version") 159 * @param parameters the parameters 160 * @param dataType the property's data type (e.g. "text") 161 * @param value the property value 162 * @throws IllegalStateException if there are no open components ( 163 * {@link #writeStartComponent(String)} must be called first) or if the last 164 * method called was {@link #writeEndComponent()}. 165 * @throws IOException if there's an I/O problem 166 */ 167 public void writeProperty(String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) throws IOException { 168 if (stack.isEmpty()) { 169 throw new IllegalStateException("Call \"writeStartComponent\" first."); 170 } 171 if (componentEnded) { 172 throw new IllegalStateException("Cannot write a property after calling \"writeEndComponent\"."); 173 } 174 175 generator.writeStartArray(); 176 indent(stack.size() * 2); 177 178 //write the property name 179 generator.writeString(propertyName); 180 181 //write parameters 182 generator.writeStartObject(); 183 for (Map.Entry<String, List<String>> entry : parameters) { 184 String name = entry.getKey().toLowerCase(); 185 List<String> values = entry.getValue(); 186 if (values.isEmpty()) { 187 continue; 188 } 189 190 if (values.size() == 1) { 191 generator.writeStringField(name, values.get(0)); 192 } else { 193 generator.writeArrayFieldStart(name); 194 for (String paramValue : values) { 195 generator.writeString(paramValue); 196 } 197 generator.writeEndArray(); 198 } 199 } 200 generator.writeEndObject(); 201 202 //write data type 203 generator.writeString((dataType == null) ? "unknown" : dataType.getName().toLowerCase()); 204 205 //write value 206 for (JsonValue jsonValue : value.getValues()) { 207 writeValue(jsonValue); 208 } 209 210 generator.writeEndArray(); 211 } 212 213 private void writeValue(JsonValue jsonValue) throws IOException { 214 if (jsonValue.isNull()) { 215 generator.writeNull(); 216 return; 217 } 218 219 Object val = jsonValue.getValue(); 220 if (val != null) { 221 if (val instanceof Byte) { 222 generator.writeNumber((Byte) val); 223 } else if (val instanceof Short) { 224 generator.writeNumber((Short) val); 225 } else if (val instanceof Integer) { 226 generator.writeNumber((Integer) val); 227 } else if (val instanceof Long) { 228 generator.writeNumber((Long) val); 229 } else if (val instanceof Float) { 230 generator.writeNumber((Float) val); 231 } else if (val instanceof Double) { 232 generator.writeNumber((Double) val); 233 } else if (val instanceof Boolean) { 234 generator.writeBoolean((Boolean) val); 235 } else { 236 generator.writeString(val.toString()); 237 } 238 return; 239 } 240 241 List<JsonValue> array = jsonValue.getArray(); 242 if (array != null) { 243 generator.writeStartArray(); 244 for (JsonValue element : array) { 245 writeValue(element); 246 } 247 generator.writeEndArray(); 248 return; 249 } 250 251 Map<String, JsonValue> object = jsonValue.getObject(); 252 if (object != null) { 253 generator.writeStartObject(); 254 for (Map.Entry<String, JsonValue> entry : object.entrySet()) { 255 generator.writeFieldName(entry.getKey()); 256 writeValue(entry.getValue()); 257 } 258 generator.writeEndObject(); 259 return; 260 } 261 } 262 263 /** 264 * Checks to see if pretty-printing is enabled, and adds indentation 265 * whitespace if it is. 266 * @param spaces the number of spaces to indent with 267 * @throws IOException 268 */ 269 private void indent(int spaces) throws IOException { 270 if (!indent) { 271 return; 272 } 273 274 generator.writeRaw(NEWLINE); 275 for (int i = 0; i < spaces; i++) { 276 generator.writeRaw(' '); 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 (generator == null) { 286 return; 287 } 288 289 generator.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 (generator == null) { 299 return; 300 } 301 302 while (!stack.isEmpty()) { 303 writeEndComponent(); 304 } 305 306 if (wrapInArray) { 307 indent(0); 308 generator.writeEndArray(); 309 } 310 311 generator.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 (generator == 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 generator = factory.createGenerator(writer); 332 333 if (wrapInArray) { 334 generator.writeStartArray(); 335 indent(0); 336 } 337 } 338 339 private static class Info { 340 public boolean wroteEndPropertiesArray = false; 341 public boolean wroteStartSubComponentsArray = false; 342 } 343}