001package biweekly.io.text; 002 003import static biweekly.util.IOUtils.utf8Writer; 004 005import java.io.Closeable; 006import java.io.File; 007import java.io.FileNotFoundException; 008import java.io.Flushable; 009import java.io.IOException; 010import java.io.OutputStream; 011import java.io.Writer; 012 013import biweekly.ICalDataType; 014import biweekly.ICalendar; 015import biweekly.component.ICalComponent; 016import biweekly.io.SkipMeException; 017import biweekly.io.scribe.ScribeIndex; 018import biweekly.io.scribe.component.ICalComponentScribe; 019import biweekly.io.scribe.property.ICalPropertyScribe; 020import biweekly.parameter.ICalParameters; 021import biweekly.property.ICalProperty; 022 023/* 024 Copyright (c) 2013, Michael Angstadt 025 All rights reserved. 026 027 Redistribution and use in source and binary forms, with or without 028 modification, are permitted provided that the following conditions are met: 029 030 1. Redistributions of source code must retain the above copyright notice, this 031 list of conditions and the following disclaimer. 032 2. Redistributions in binary form must reproduce the above copyright notice, 033 this list of conditions and the following disclaimer in the documentation 034 and/or other materials provided with the distribution. 035 036 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 037 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 038 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 039 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 040 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 041 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 042 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 043 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 044 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 045 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 046 */ 047 048/** 049 * <p> 050 * Writes {@link ICalendar} objects to an iCalendar data stream. 051 * </p> 052 * <p> 053 * <b>Example:</b> 054 * 055 * <pre class="brush:java"> 056 * List<ICalendar> icals = ... 057 * OutputStream out = ... 058 * ICalWriter icalWriter = new ICalWriter(out); 059 * for (ICalendar ical : icals){ 060 * icalWriter.write(ical); 061 * } 062 * icalWriter.close(); 063 * </pre> 064 * 065 * </p> 066 * @author Michael Angstadt 067 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a> 068 */ 069public class ICalWriter implements Closeable, Flushable { 070 private ScribeIndex index = new ScribeIndex(); 071 private final ICalRawWriter writer; 072 073 /** 074 * Creates an iCalendar writer that writes to an output stream. Uses the 075 * standard folding scheme and newline sequence. 076 * @param outputStream the output stream to write to 077 */ 078 public ICalWriter(OutputStream outputStream) { 079 this(utf8Writer(outputStream)); 080 } 081 082 /** 083 * Creates an iCalendar writer that writes to an output stream. Uses the 084 * standard newline sequence. 085 * @param outputStream the output stream to write to 086 * @param foldingScheme the folding scheme to use or null not to fold at all 087 */ 088 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme) { 089 this(utf8Writer(outputStream), foldingScheme); 090 } 091 092 /** 093 * Creates an iCalendar writer that writes to an output stream. 094 * @param outputStream the output stream to write to 095 * @param foldingScheme the folding scheme to use or null not to fold at all 096 * @param newline the newline sequence to use 097 */ 098 public ICalWriter(OutputStream outputStream, FoldingScheme foldingScheme, String newline) { 099 this(utf8Writer(outputStream), foldingScheme, newline); 100 } 101 102 /** 103 * Creates an iCalendar writer that writes to a file. Uses the standard 104 * folding scheme and newline sequence. 105 * @param file the file to write to 106 * @throws FileNotFoundException if the file cannot be written to 107 */ 108 public ICalWriter(File file) throws FileNotFoundException { 109 this(utf8Writer(file)); 110 } 111 112 /** 113 * Creates an iCalendar writer that writes to a file. Uses the standard 114 * folding scheme and newline sequence. 115 * @param file the file to write to 116 * @param append true to append to the end of the file, false to overwrite 117 * it 118 * @throws FileNotFoundException if the file cannot be written to 119 */ 120 public ICalWriter(File file, boolean append) throws FileNotFoundException { 121 this(utf8Writer(file, append)); 122 } 123 124 /** 125 * Creates an iCalendar writer that writes to a file. Uses the standard 126 * newline sequence. 127 * @param file the file to write to 128 * @param append true to append to the end of the file, false to overwrite 129 * it 130 * @param foldingScheme the folding scheme to use or null not to fold at all 131 * @throws FileNotFoundException if the file cannot be written to 132 */ 133 public ICalWriter(File file, boolean append, FoldingScheme foldingScheme) throws FileNotFoundException { 134 this(utf8Writer(file, append), foldingScheme); 135 } 136 137 /** 138 * Creates an iCalendar writer that writes to a file. 139 * @param file the file to write to 140 * @param append true to append to the end of the file, false to overwrite 141 * it 142 * @param foldingScheme the folding scheme to use or null not to fold at all 143 * @param newline the newline sequence to use 144 * @throws FileNotFoundException if the file cannot be written to 145 */ 146 public ICalWriter(File file, boolean append, FoldingScheme foldingScheme, String newline) throws FileNotFoundException { 147 this(utf8Writer(file, append), foldingScheme, newline); 148 } 149 150 /** 151 * Creates an iCalendar writer that writes to a writer. Uses the standard 152 * folding scheme and newline sequence. 153 * @param writer the writer to the data stream 154 */ 155 public ICalWriter(Writer writer) { 156 this(writer, FoldingScheme.DEFAULT); 157 } 158 159 /** 160 * Creates an iCalendar writer that writes to a writer. Uses the standard 161 * newline sequence. 162 * @param writer the writer to the data stream 163 * @param foldingScheme the folding scheme to use or null not to fold at all 164 */ 165 public ICalWriter(Writer writer, FoldingScheme foldingScheme) { 166 this(writer, foldingScheme, "\r\n"); 167 } 168 169 /** 170 * Creates an iCalendar writer that writes to a writer. 171 * @param writer the writer to the data stream 172 * @param foldingScheme the folding scheme to use or null not to fold at all 173 * @param newline the newline sequence to use 174 */ 175 public ICalWriter(Writer writer, FoldingScheme foldingScheme, String newline) { 176 this.writer = new ICalRawWriter(writer, foldingScheme, newline); 177 } 178 179 /** 180 * <p> 181 * Gets whether the writer will apply circumflex accent encoding on 182 * parameter values (disabled by default). This escaping mechanism allows 183 * for newlines and double quotes to be included in parameter values. 184 * </p> 185 * 186 * <p> 187 * When disabled, the writer will replace newlines with spaces and double 188 * quotes with single quotes. 189 * </p> 190 * @return true if circumflex accent encoding is enabled, false if not 191 * @see ICalRawWriter#isCaretEncodingEnabled() 192 */ 193 public boolean isCaretEncodingEnabled() { 194 return writer.isCaretEncodingEnabled(); 195 } 196 197 /** 198 * <p> 199 * Sets whether the writer will apply circumflex accent encoding on 200 * parameter values (disabled by default). This escaping mechanism allows 201 * for newlines and double quotes to be included in parameter values. 202 * </p> 203 * 204 * <p> 205 * When disabled, the writer will replace newlines with spaces and double 206 * quotes with single quotes. 207 * </p> 208 * @param enable true to use circumflex accent encoding, false not to 209 * @see ICalRawWriter#setCaretEncodingEnabled(boolean) 210 */ 211 public void setCaretEncodingEnabled(boolean enable) { 212 writer.setCaretEncodingEnabled(enable); 213 } 214 215 /** 216 * Gets the newline sequence that is used to separate lines. 217 * @return the newline sequence 218 */ 219 public String getNewline() { 220 return writer.getNewline(); 221 } 222 223 /** 224 * Gets the rules for how each line is folded. 225 * @return the folding scheme or null if the lines are not folded 226 */ 227 public FoldingScheme getFoldingScheme() { 228 return writer.getFoldingScheme(); 229 } 230 231 /** 232 * <p> 233 * Registers an experimental property scribe. Can also be used to override 234 * the scribe of a standard property (such as DTSTART). Calling this method 235 * is the same as calling: 236 * </p> 237 * <p> 238 * {@code getScribeIndex().register(scribe)}. 239 * </p> 240 * @param scribe the scribe to register 241 */ 242 public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) { 243 index.register(scribe); 244 } 245 246 /** 247 * <p> 248 * Registers an experimental component scribe. Can also be used to override 249 * the scribe of a standard component (such as VEVENT). Calling this method 250 * is the same as calling: 251 * </p> 252 * <p> 253 * {@code getScribeIndex().register(scribe)}. 254 * </p> 255 * @param scribe the scribe to register 256 */ 257 public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) { 258 index.register(scribe); 259 } 260 261 /** 262 * Gets the object that manages the component/property scribes. 263 * @return the scribe index 264 */ 265 public ScribeIndex getScribeIndex() { 266 return index; 267 } 268 269 /** 270 * Sets the object that manages the component/property scribes. 271 * @param scribe the scribe index 272 */ 273 public void setScribeIndex(ScribeIndex scribe) { 274 this.index = scribe; 275 } 276 277 /** 278 * Writes an iCalendar object to the data stream. 279 * @param ical the iCalendar object to write 280 * @throws IllegalArgumentException if the scribe class for a component or 281 * property object cannot be found (only happens when an experimental 282 * property/component scribe is not registered with the 283 * {@code registerScribe} method.) 284 * @throws IOException if there's a problem writing to the data stream 285 */ 286 public void write(ICalendar ical) throws IOException { 287 index.hasScribesFor(ical); 288 writeComponent(ical); 289 } 290 291 /** 292 * Writes a component to the data stream. 293 * @param component the component to write 294 * @throws IOException if there's a problem writing to the data stream 295 */ 296 @SuppressWarnings({ "rawtypes", "unchecked" }) 297 private void writeComponent(ICalComponent component) throws IOException { 298 ICalComponentScribe componentScribe = index.getComponentScribe(component); 299 writer.writeBeginComponent(componentScribe.getComponentName()); 300 301 for (Object propertyObj : componentScribe.getProperties(component)) { 302 ICalProperty property = (ICalProperty) propertyObj; 303 ICalPropertyScribe propertyScribe = index.getPropertyScribe(property); 304 305 //marshal property 306 ICalParameters parameters; 307 String value; 308 try { 309 parameters = propertyScribe.prepareParameters(property); 310 value = propertyScribe.writeText(property); 311 } catch (SkipMeException e) { 312 continue; 313 } 314 315 //set the data type 316 ICalDataType dataType = propertyScribe.dataType(property); 317 if (dataType != null && dataType != propertyScribe.getDefaultDataType()) { 318 //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type 319 parameters.setValue(dataType); 320 } 321 322 //write property to data stream 323 writer.writeProperty(propertyScribe.getPropertyName(), parameters, value); 324 } 325 326 for (Object subComponentObj : componentScribe.getComponents(component)) { 327 ICalComponent subComponent = (ICalComponent) subComponentObj; 328 writeComponent(subComponent); 329 } 330 331 writer.writeEndComponent(componentScribe.getComponentName()); 332 } 333 334 /** 335 * Flushes the stream. 336 * @throws IOException if there's a problem flushing the stream 337 */ 338 public void flush() throws IOException { 339 writer.flush(); 340 } 341 342 /** 343 * Closes the underlying {@link Writer} object. 344 */ 345 public void close() throws IOException { 346 writer.close(); 347 } 348}