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