001package biweekly.io.text; 002 003import static biweekly.io.DataModelConverter.convert; 004import static biweekly.util.IOUtils.utf8Writer; 005 006import java.io.File; 007import java.io.FileWriter; 008import java.io.Flushable; 009import java.io.IOException; 010import java.io.OutputStream; 011import java.io.OutputStreamWriter; 012import java.io.Writer; 013import java.util.Collection; 014import java.util.List; 015 016import biweekly.ICalDataType; 017import biweekly.ICalVersion; 018import biweekly.ICalendar; 019import biweekly.component.ICalComponent; 020import biweekly.component.VAlarm; 021import biweekly.component.VTimezone; 022import biweekly.io.DataModelConverter.VCalTimezoneProperties; 023import biweekly.io.SkipMeException; 024import biweekly.io.StreamWriter; 025import biweekly.io.scribe.component.ICalComponentScribe; 026import biweekly.io.scribe.property.ICalPropertyScribe; 027import biweekly.parameter.ICalParameters; 028import biweekly.property.Attendee; 029import biweekly.property.Created; 030import biweekly.property.DateTimeStamp; 031import biweekly.property.Daylight; 032import biweekly.property.ICalProperty; 033import biweekly.property.Organizer; 034import biweekly.property.Timezone; 035import biweekly.property.VCalAlarmProperty; 036import biweekly.property.Version; 037 038/* 039 Copyright (c) 2013-2015, Michael Angstadt 040 All rights reserved. 041 042 Redistribution and use in source and binary forms, with or without 043 modification, are permitted provided that the following conditions are met: 044 045 1. Redistributions of source code must retain the above copyright notice, this 046 list of conditions and the following disclaimer. 047 2. Redistributions in binary form must reproduce the above copyright notice, 048 this list of conditions and the following disclaimer in the documentation 049 and/or other materials provided with the distribution. 050 051 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 052 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 053 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 054 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 055 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 056 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 057 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 058 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 059 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 060 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 061 */ 062 063/** 064 * <p> 065 * Writes {@link ICalendar} objects to an iCalendar data stream. 066 * </p> 067 * <p> 068 * <b>Example:</b> 069 * 070 * <pre class="brush:java"> 071 * List<ICalendar> icals = ... 072 * OutputStream out = ... 073 * ICalWriter icalWriter = new ICalWriter(out, ICalVersion.V2_0); 074 * for (ICalendar ical : icals){ 075 * icalWriter.write(ical); 076 * } 077 * icalWriter.close(); 078 * </pre> 079 * 080 * </p> 081 * 082 * <p> 083 * <b>Changing the timezone settings:</b> 084 * 085 * <pre class="brush:java"> 086 * ICalWriter writer = new ICalWriter(...); 087 * 088 * //format all date/time values in a specific timezone instead of UTC 089 * //note: this makes an HTTP call to "http://tzurl.org" 090 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault()); 091 * 092 * //format the value of a single date/time property in a specific timezone instead of UTC 093 * //note: this makes an HTTP call to "http://tzurl.org" 094 * DateStart dtstart = ... 095 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault()); 096 * 097 * //generate Outlook-friendly VTIMEZONE components: 098 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true)); 099 * </pre> 100 * 101 * </p> 102 * 103 * <p> 104 * <b>Changing the line folding settings:</b> 105 * 106 * <pre class="brush:java"> 107 * ICalWriter writer = new ICalWriter(...); 108 * 109 * //disable line folding 110 * writer.getRawWriter().getFoldedLineWriter().setLineLength(null); 111 * 112 * //set line length (defaults to 75) 113 * writer.getRawWriter().getFoldedLineWriter().setLineLength(50); 114 * 115 * //change folded line indent string (defaults to one space character) 116 * writer.getRawWriter().getFoldedLineWriter().setIndent("\t"); 117 * 118 * //change newline character (defaults to CRLF) 119 * writer.getRawWriter().getFoldedLineWriter().setNewline("**"); 120 * </pre> 121 * 122 * </p> 123 * @author Michael Angstadt 124 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a> 125 */ 126public class ICalWriter extends StreamWriter implements Flushable { 127 private final ICalRawWriter writer; 128 129 /** 130 * Creates an iCalendar writer that writes to an output stream. 131 * @param outputStream the output stream to write to 132 * @param version the iCalendar version to adhere to 133 */ 134 public ICalWriter(OutputStream outputStream, ICalVersion version) { 135 this((version == ICalVersion.V1_0) ? new OutputStreamWriter(outputStream) : utf8Writer(outputStream), version); 136 } 137 138 /** 139 * Creates an iCalendar writer that writes to a file. 140 * @param file the file to write to 141 * @param version the iCalendar version to adhere to 142 * @throws IOException if the file cannot be written to 143 */ 144 public ICalWriter(File file, ICalVersion version) throws IOException { 145 this(file, false, version); 146 } 147 148 /** 149 * Creates an iCalendar writer that writes to a file. 150 * @param file the file to write to 151 * @param version the iCalendar version to adhere to 152 * @param append true to append to the end of the file, false to overwrite 153 * it 154 * @throws IOException if the file cannot be written to 155 */ 156 public ICalWriter(File file, boolean append, ICalVersion version) throws IOException { 157 this((version == ICalVersion.V1_0) ? new FileWriter(file, append) : utf8Writer(file, append), version); 158 } 159 160 /** 161 * Creates an iCalendar writer that writes to a writer. 162 * @param writer the output stream to write to 163 * @param version the iCalendar version to adhere to 164 */ 165 public ICalWriter(Writer writer, ICalVersion version) { 166 this.writer = new ICalRawWriter(writer, version); 167 } 168 169 /** 170 * Gets the writer object that is used internally to write to the output 171 * stream. 172 * @return the raw writer 173 */ 174 public ICalRawWriter getRawWriter() { 175 return writer; 176 } 177 178 /** 179 * Gets the version that the written iCalendar objects will adhere to. 180 * @return the iCalendar version 181 */ 182 @Override 183 public ICalVersion getTargetVersion() { 184 return writer.getVersion(); 185 } 186 187 /** 188 * Sets the version that the written iCalendar objects will adhere to. 189 * @param targetVersion the iCalendar version 190 */ 191 public void setTargetVersion(ICalVersion targetVersion) { 192 writer.setVersion(targetVersion); 193 } 194 195 /** 196 * <p> 197 * Gets whether the writer will apply circumflex accent encoding on 198 * parameter values (disabled by default). This escaping mechanism allows 199 * for newlines and double quotes to be included in parameter values. 200 * </p> 201 * 202 * <p> 203 * When disabled, the writer will replace newlines with spaces and double 204 * quotes with single quotes. 205 * </p> 206 * @return true if circumflex accent encoding is enabled, false if not 207 * @see ICalRawWriter#isCaretEncodingEnabled() 208 */ 209 public boolean isCaretEncodingEnabled() { 210 return writer.isCaretEncodingEnabled(); 211 } 212 213 /** 214 * <p> 215 * Sets whether the writer will apply circumflex accent encoding on 216 * parameter values (disabled by default). This escaping mechanism allows 217 * for newlines and double quotes to be included in parameter values. 218 * </p> 219 * 220 * <p> 221 * When disabled, the writer will replace newlines with spaces and double 222 * quotes with single quotes. 223 * </p> 224 * @param enable true to use circumflex accent encoding, false not to 225 * @see ICalRawWriter#setCaretEncodingEnabled(boolean) 226 */ 227 public void setCaretEncodingEnabled(boolean enable) { 228 writer.setCaretEncodingEnabled(enable); 229 } 230 231 @Override 232 protected void _write(ICalendar ical) throws IOException { 233 writeComponent(ical, null); 234 } 235 236 /** 237 * Writes a component to the data stream. 238 * @param component the component to write 239 * @param parent the parent component 240 * @throws IOException if there's a problem writing to the data stream 241 */ 242 @SuppressWarnings({ "rawtypes", "unchecked" }) 243 private void writeComponent(ICalComponent component, ICalComponent parent) throws IOException { 244 switch (writer.getVersion()) { 245 case V1_0: 246 //VALARM component => vCal alarm property 247 if (component instanceof VAlarm) { 248 VAlarm valarm = (VAlarm) component; 249 VCalAlarmProperty vcalAlarm = convert(valarm, component); 250 if (vcalAlarm != null) { 251 writeProperty(vcalAlarm); 252 return; 253 } 254 } 255 256 break; 257 258 default: 259 //empty 260 break; 261 } 262 263 boolean inICalendar = component instanceof ICalendar; 264 boolean inVCalRoot = inICalendar && getTargetVersion() == ICalVersion.V1_0; 265 boolean inICalRoot = inICalendar && getTargetVersion() != ICalVersion.V1_0; 266 267 ICalComponentScribe componentScribe = index.getComponentScribe(component); 268 writer.writeBeginComponent(componentScribe.getComponentName()); 269 270 List propertyObjs = componentScribe.getProperties(component); 271 if (inICalendar && component.getProperty(Version.class) == null) { 272 propertyObjs.add(0, new Version(getTargetVersion())); 273 } 274 275 for (Object propertyObj : propertyObjs) { 276 context.setParent(component); //set parent here incase a scribe resets the parent 277 ICalProperty property = (ICalProperty) propertyObj; 278 writeProperty(property); 279 } 280 281 Collection subComponents = componentScribe.getComponents(component); 282 if (inICalRoot) { 283 //add the VTIMEZONE components 284 Collection<VTimezone> timezones = tzinfo.getComponents(); 285 for (VTimezone timezone : timezones) { 286 if (!subComponents.contains(timezone)) { 287 subComponents.add(timezone); 288 } 289 } 290 } 291 292 for (Object subComponentObj : subComponents) { 293 ICalComponent subComponent = (ICalComponent) subComponentObj; 294 writeComponent(subComponent, component); 295 } 296 297 if (inVCalRoot) { 298 Collection<VTimezone> timezones = tzinfo.getComponents(); 299 if (!timezones.isEmpty()) { 300 VTimezone timezone = timezones.iterator().next(); 301 VCalTimezoneProperties props = convert(timezone, context.getDates()); 302 303 Timezone tz = props.getTz(); 304 if (tz != null) { 305 writeProperty(tz); 306 } 307 for (Daylight daylight : props.getDaylights()) { 308 writeProperty(daylight); 309 } 310 } 311 } 312 313 writer.writeEndComponent(componentScribe.getComponentName()); 314 } 315 316 @SuppressWarnings({ "rawtypes", "unchecked" }) 317 private void writeProperty(ICalProperty property) throws IOException { 318 switch (writer.getVersion()) { 319 case V1_0: 320 //ORGANIZER property => ATTENDEE with role of "organizer" 321 if (property instanceof Organizer) { 322 Organizer organizer = (Organizer) property; 323 Attendee attendee = convert(organizer); 324 writeProperty(attendee); 325 return; 326 } 327 if (property instanceof DateTimeStamp) { 328 //do not write this property 329 return; 330 } 331 break; 332 333 default: 334 //empty 335 break; 336 } 337 338 ICalPropertyScribe scribe = index.getPropertyScribe(property); 339 340 //marshal property 341 String value; 342 try { 343 value = scribe.writeText(property, context); 344 } catch (SkipMeException e) { 345 return; 346 } 347 348 //get parameters 349 ICalParameters parameters = scribe.prepareParameters(property, context); 350 351 //set the data type 352 ICalDataType dataType = scribe.dataType(property, writer.getVersion()); 353 if (dataType != null && dataType != scribe.defaultDataType(writer.getVersion())) { 354 //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type 355 parameters = new ICalParameters(parameters); 356 parameters.setValue(dataType); 357 } 358 359 //get the property name 360 String propertyName; 361 if (writer.getVersion() == ICalVersion.V1_0 && property instanceof Created) { 362 //the vCal DCREATED property is the same as the iCal CREATED property 363 propertyName = "DCREATED"; 364 } else { 365 propertyName = scribe.getPropertyName(); 366 } 367 368 //write property to data stream 369 writer.writeProperty(propertyName, parameters, value); 370 } 371 372 /** 373 * Flushes the stream. 374 * @throws IOException if there's a problem flushing the stream 375 */ 376 public void flush() throws IOException { 377 writer.flush(); 378 } 379 380 /** 381 * Closes the underlying {@link Writer} object. 382 */ 383 public void close() throws IOException { 384 writer.close(); 385 } 386}