001package biweekly.io.json; 002 003import static biweekly.util.IOUtils.utf8Writer; 004 005import java.io.File; 006import java.io.Flushable; 007import java.io.IOException; 008import java.io.OutputStream; 009import java.io.Writer; 010import java.util.Collection; 011import java.util.List; 012 013import biweekly.ICalDataType; 014import biweekly.ICalVersion; 015import biweekly.ICalendar; 016import biweekly.component.ICalComponent; 017import biweekly.component.VTimezone; 018import biweekly.io.SkipMeException; 019import biweekly.io.StreamWriter; 020import biweekly.io.scribe.component.ICalComponentScribe; 021import biweekly.io.scribe.property.ICalPropertyScribe; 022import biweekly.parameter.ICalParameters; 023import biweekly.property.ICalProperty; 024import biweekly.property.Version; 025 026/* 027 Copyright (c) 2013-2015, Michael Angstadt 028 All rights reserved. 029 030 Redistribution and use in source and binary forms, with or without 031 modification, are permitted provided that the following conditions are met: 032 033 1. Redistributions of source code must retain the above copyright notice, this 034 list of conditions and the following disclaimer. 035 2. Redistributions in binary form must reproduce the above copyright notice, 036 this list of conditions and the following disclaimer in the documentation 037 and/or other materials provided with the distribution. 038 039 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 040 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 041 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 042 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 043 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 044 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 045 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 046 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 047 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 048 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 049 */ 050 051/** 052 * <p> 053 * Writes {@link ICalendar} objects to a JSON data stream (jCal). 054 * </p> 055 * <p> 056 * <b>Example:</b> 057 * 058 * <pre class="brush:java"> 059 * List<ICalendar> icals = ... 060 * OutputStream out = ... 061 * JCalWriter jcalWriter = new JCalWriter(out); 062 * for (ICalendar ical : icals){ 063 * jcalWriter.write(ical); 064 * } 065 * jcalWriter.close(); 066 * </pre> 067 * 068 * </p> 069 * 070 * <p> 071 * <b>Changing the timezone settings:</b> 072 * 073 * <pre class="brush:java"> 074 * JCalWriter writer = new JCalWriter(...); 075 * 076 * //format all date/time values in a specific timezone instead of UTC 077 * //note: this makes an HTTP call to the "tzurl.org" website 078 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault()); 079 * 080 * //format the value of a particular date/time property in a specific timezone instead of UTC 081 * //note: this makes an HTTP call to the "tzurl.org" website 082 * DateStart dtstart = ... 083 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault()); 084 * 085 * //generate Outlook-friendly VTIMEZONE components: 086 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true)); 087 * </pre> 088 * 089 * </p> 090 * @author Michael Angstadt 091 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a> 092 */ 093public class JCalWriter extends StreamWriter implements Flushable { 094 private final JCalRawWriter writer; 095 private final ICalVersion targetVersion = ICalVersion.V2_0; 096 097 /** 098 * Creates a jCal writer that writes to an output stream. 099 * @param outputStream the output stream to write to 100 */ 101 public JCalWriter(OutputStream outputStream) { 102 this(utf8Writer(outputStream)); 103 } 104 105 /** 106 * Creates a jCal writer that writes to an output stream. 107 * @param outputStream the output stream to write to 108 * @param wrapInArray true to wrap all iCalendar objects in a parent array, 109 * false not to (useful when writing more than one iCalendar object) 110 */ 111 public JCalWriter(OutputStream outputStream, boolean wrapInArray) { 112 this(utf8Writer(outputStream), wrapInArray); 113 } 114 115 /** 116 * Creates a jCal writer that writes to a file. 117 * @param file the file to write to 118 * @throws IOException if the file cannot be written to 119 */ 120 public JCalWriter(File file) throws IOException { 121 this(utf8Writer(file)); 122 } 123 124 /** 125 * Creates a jCal writer that writes to a file. 126 * @param file the file to write to 127 * @param wrapInArray true to wrap all iCalendar objects in a parent array, 128 * false not to (useful when writing more than one iCalendar object) 129 * @throws IOException if the file cannot be written to 130 */ 131 public JCalWriter(File file, boolean wrapInArray) throws IOException { 132 this(utf8Writer(file), wrapInArray); 133 } 134 135 /** 136 * Creates a jCal writer that writes to a writer. 137 * @param writer the writer to the data stream 138 */ 139 public JCalWriter(Writer writer) { 140 this(writer, false); 141 } 142 143 /** 144 * Creates a jCal writer that writes to a writer. 145 * @param writer the writer to the data stream 146 * @param wrapInArray true to wrap all iCalendar objects in a parent array, 147 * false not to (useful when writing more than one iCalendar object) 148 */ 149 public JCalWriter(Writer writer, boolean wrapInArray) { 150 this.writer = new JCalRawWriter(writer, wrapInArray); 151 } 152 153 /** 154 * Gets whether or not the JSON will be pretty-printed. 155 * @return true if it will be pretty-printed, false if not (defaults to 156 * false) 157 */ 158 public boolean isIndent() { 159 return writer.isIndent(); 160 } 161 162 /** 163 * Sets whether or not to pretty-print the JSON. 164 * @param indent true to pretty-print it, false not to (defaults to false) 165 */ 166 public void setIndent(boolean indent) { 167 writer.setIndent(indent); 168 } 169 170 @Override 171 protected void _write(ICalendar ical) throws IOException { 172 writeComponent(ical); 173 } 174 175 @Override 176 protected ICalVersion getTargetVersion() { 177 return targetVersion; 178 } 179 180 /** 181 * Writes a component to the data stream. 182 * @param component the component to write 183 * @throws IllegalArgumentException if the scribe class for a component or 184 * property object cannot be found (only happens when an experimental 185 * property/component scribe is not registered with the 186 * {@code registerScribe} method.) 187 * @throws IOException if there's a problem writing to the data stream 188 */ 189 @SuppressWarnings({ "rawtypes", "unchecked" }) 190 private void writeComponent(ICalComponent component) throws IOException { 191 ICalComponentScribe componentScribe = index.getComponentScribe(component); 192 writer.writeStartComponent(componentScribe.getComponentName().toLowerCase()); 193 194 List propertyObjs = componentScribe.getProperties(component); 195 if (component instanceof ICalendar && component.getProperty(Version.class) == null) { 196 propertyObjs.add(0, new Version(targetVersion)); 197 } 198 199 //write properties 200 for (Object propertyObj : propertyObjs) { 201 context.setParent(component); //set parent here incase a scribe resets the parent 202 ICalProperty property = (ICalProperty) propertyObj; 203 ICalPropertyScribe propertyScribe = index.getPropertyScribe(property); 204 205 //marshal property 206 ICalParameters parameters; 207 JCalValue value; 208 try { 209 parameters = propertyScribe.prepareParameters(property, context); 210 value = propertyScribe.writeJson(property, context); 211 } catch (SkipMeException e) { 212 continue; 213 } 214 215 //write property 216 String propertyName = propertyScribe.getPropertyName().toLowerCase(); 217 ICalDataType dataType = propertyScribe.dataType(property, targetVersion); 218 writer.writeProperty(propertyName, parameters, dataType, value); 219 } 220 221 //write sub-components 222 Collection subComponents = componentScribe.getComponents(component); 223 if (component instanceof ICalendar) { 224 //add the VTIMEZONE components that were auto-generated by TimezoneOptions 225 Collection<VTimezone> tzs = tzinfo.getComponents(); 226 for (VTimezone tz : tzs) { 227 if (!subComponents.contains(tz)) { 228 subComponents.add(tz); 229 } 230 } 231 } 232 for (Object subComponentObj : subComponents) { 233 ICalComponent subComponent = (ICalComponent) subComponentObj; 234 writeComponent(subComponent); 235 } 236 237 writer.writeEndComponent(); 238 } 239 240 /** 241 * Flushes the stream. 242 * @throws IOException if there's a problem flushing the stream 243 */ 244 public void flush() throws IOException { 245 writer.flush(); 246 } 247 248 /** 249 * Finishes writing the JSON document and closes the underlying 250 * {@link Writer} object. 251 * @throws IOException if there's a problem closing the stream 252 */ 253 public void close() throws IOException { 254 writer.close(); 255 } 256 257 /** 258 * Finishes writing the JSON document so that it is syntactically correct. 259 * No more iCalendar objects can be written once this method is called. 260 * @throws IOException if there's a problem writing to the data stream 261 */ 262 public void closeJsonStream() throws IOException { 263 writer.closeJsonStream(); 264 } 265}