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 }