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 a plain-text iCalendar data stream. 066 * </p> 067 * <p> 068 * <b>Example:</b> 069 * 070 * <pre class="brush:java"> 071 * ICalendar ical1 = ... 072 * ICalendar ical2 = ... 073 * File file = new File("icals.ics"); 074 * ICalWriter writer = null; 075 * try { 076 * writer = new ICalWriter(file, ICalVersion.V2_0); 077 * writer.write(ical1); 078 * writer.write(ical2); 079 * } finally { 080 * if (writer != null) writer.close(); 081 * } 082 * </pre> 083 * 084 * </p> 085 * 086 * <p> 087 * <b>Changing the timezone settings:</b> 088 * 089 * <pre class="brush:java"> 090 * ICalWriter writer = new ICalWriter(...); 091 * 092 * //format all date/time values in a specific timezone instead of UTC 093 * //note: this makes an HTTP call to "http://tzurl.org" 094 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault()); 095 * 096 * //format the value of a single date/time property in a specific timezone instead of UTC 097 * //note: this makes an HTTP call to "http://tzurl.org" 098 * DateStart dtstart = ... 099 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault()); 100 * 101 * //generate Outlook-friendly VTIMEZONE components: 102 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true)); 103 * </pre> 104 * 105 * </p> 106 * 107 * <p> 108 * <b>Changing the line folding settings:</b> 109 * 110 * <pre class="brush:java"> 111 * ICalWriter writer = new ICalWriter(...); 112 * 113 * //disable line folding 114 * writer.getRawWriter().getFoldedLineWriter().setLineLength(null); 115 * 116 * //set line length (defaults to 75) 117 * writer.getRawWriter().getFoldedLineWriter().setLineLength(50); 118 * 119 * //change folded line indent string (defaults to one space character) 120 * writer.getRawWriter().getFoldedLineWriter().setIndent("\t"); 121 * 122 * //change newline character (defaults to CRLF) 123 * writer.getRawWriter().getFoldedLineWriter().setNewline("**"); 124 * </pre> 125 * 126 * </p> 127 * @author Michael Angstadt 128 * @see <a href="http://www.imc.org/pdi/pdiproddev.html">1.0 specs</a> 129 * @see <a href="https://tools.ietf.org/html/rfc2445">RFC 2445</a> 130 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a> 131 */ 132public class ICalWriter extends StreamWriter implements Flushable { 133 private final ICalRawWriter writer; 134 135 /** 136 * @param out the output stream to write to 137 * @param version the iCalendar version to adhere to 138 */ 139 public ICalWriter(OutputStream out, ICalVersion version) { 140 this((version == ICalVersion.V1_0) ? new OutputStreamWriter(out) : utf8Writer(out), version); 141 } 142 143 /** 144 * @param file the file to write to 145 * @param version the iCalendar version to adhere to 146 * @throws IOException if the file cannot be written to 147 */ 148 public ICalWriter(File file, ICalVersion version) throws IOException { 149 this(file, false, version); 150 } 151 152 /** 153 * @param file the file to write to 154 * @param version the iCalendar version to adhere to 155 * @param append true to append to the end of the file, false to overwrite 156 * it 157 * @throws IOException if the file cannot be written to 158 */ 159 public ICalWriter(File file, boolean append, ICalVersion version) throws IOException { 160 this((version == ICalVersion.V1_0) ? new FileWriter(file, append) : utf8Writer(file, append), version); 161 } 162 163 /** 164 * @param writer the writer to write to 165 * @param version the iCalendar version to adhere to 166 */ 167 public ICalWriter(Writer writer, ICalVersion version) { 168 this.writer = new ICalRawWriter(writer, version); 169 } 170 171 /** 172 * Gets the writer object that is used internally to write to the output 173 * stream. 174 * @return the raw writer 175 */ 176 public ICalRawWriter getRawWriter() { 177 return writer; 178 } 179 180 /** 181 * Gets the version that the written iCalendar objects will adhere to. 182 * @return the iCalendar version 183 */ 184 @Override 185 public ICalVersion getTargetVersion() { 186 return writer.getVersion(); 187 } 188 189 /** 190 * Sets the version that the written iCalendar objects will adhere to. 191 * @param targetVersion the iCalendar version 192 */ 193 public void setTargetVersion(ICalVersion targetVersion) { 194 writer.setVersion(targetVersion); 195 } 196 197 /** 198 * <p> 199 * Gets 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 * @return true if circumflex accent encoding is enabled, false if not 209 * @see ICalRawWriter#isCaretEncodingEnabled() 210 */ 211 public boolean isCaretEncodingEnabled() { 212 return writer.isCaretEncodingEnabled(); 213 } 214 215 /** 216 * <p> 217 * Sets whether the writer will apply circumflex accent encoding on 218 * parameter values (disabled by default). This escaping mechanism allows 219 * for newlines and double quotes to be included in parameter values. 220 * </p> 221 * 222 * <p> 223 * When disabled, the writer will replace newlines with spaces and double 224 * quotes with single quotes. 225 * </p> 226 * @param enable true to use circumflex accent encoding, false not to 227 * @see ICalRawWriter#setCaretEncodingEnabled(boolean) 228 */ 229 public void setCaretEncodingEnabled(boolean enable) { 230 writer.setCaretEncodingEnabled(enable); 231 } 232 233 @Override 234 protected void _write(ICalendar ical) throws IOException { 235 writeComponent(ical, null); 236 } 237 238 /** 239 * Writes a component to the data stream. 240 * @param component the component to write 241 * @param parent the parent component 242 * @throws IOException if there's a problem writing to the data stream 243 */ 244 @SuppressWarnings({ "rawtypes", "unchecked" }) 245 private void writeComponent(ICalComponent component, ICalComponent parent) throws IOException { 246 switch (writer.getVersion()) { 247 case V1_0: 248 //VALARM component => vCal alarm property 249 if (component instanceof VAlarm) { 250 VAlarm valarm = (VAlarm) component; 251 VCalAlarmProperty vcalAlarm = convert(valarm, component); 252 if (vcalAlarm != null) { 253 writeProperty(vcalAlarm); 254 return; 255 } 256 } 257 258 break; 259 260 default: 261 //empty 262 break; 263 } 264 265 boolean inICalendar = component instanceof ICalendar; 266 boolean inVCalRoot = inICalendar && getTargetVersion() == ICalVersion.V1_0; 267 boolean inICalRoot = inICalendar && getTargetVersion() != ICalVersion.V1_0; 268 269 ICalComponentScribe componentScribe = index.getComponentScribe(component); 270 writer.writeBeginComponent(componentScribe.getComponentName()); 271 272 List propertyObjs = componentScribe.getProperties(component); 273 if (inICalendar && component.getProperty(Version.class) == null) { 274 propertyObjs.add(0, new Version(getTargetVersion())); 275 } 276 277 for (Object propertyObj : propertyObjs) { 278 context.setParent(component); //set parent here incase a scribe resets the parent 279 ICalProperty property = (ICalProperty) propertyObj; 280 writeProperty(property); 281 } 282 283 Collection subComponents = componentScribe.getComponents(component); 284 if (inICalRoot) { 285 //add the VTIMEZONE components 286 Collection<VTimezone> timezones = tzinfo.getComponents(); 287 for (VTimezone timezone : timezones) { 288 if (!subComponents.contains(timezone)) { 289 subComponents.add(timezone); 290 } 291 } 292 } 293 294 for (Object subComponentObj : subComponents) { 295 ICalComponent subComponent = (ICalComponent) subComponentObj; 296 writeComponent(subComponent, component); 297 } 298 299 if (inVCalRoot) { 300 Collection<VTimezone> timezones = tzinfo.getComponents(); 301 if (!timezones.isEmpty()) { 302 VTimezone timezone = timezones.iterator().next(); 303 VCalTimezoneProperties props = convert(timezone, context.getDates()); 304 305 Timezone tz = props.getTz(); 306 if (tz != null) { 307 writeProperty(tz); 308 } 309 for (Daylight daylight : props.getDaylights()) { 310 writeProperty(daylight); 311 } 312 } 313 } 314 315 writer.writeEndComponent(componentScribe.getComponentName()); 316 } 317 318 @SuppressWarnings({ "rawtypes", "unchecked" }) 319 private void writeProperty(ICalProperty property) throws IOException { 320 switch (writer.getVersion()) { 321 case V1_0: 322 //ORGANIZER property => ATTENDEE with role of "organizer" 323 if (property instanceof Organizer) { 324 Organizer organizer = (Organizer) property; 325 Attendee attendee = convert(organizer); 326 writeProperty(attendee); 327 return; 328 } 329 if (property instanceof DateTimeStamp) { 330 //do not write this property 331 return; 332 } 333 break; 334 335 default: 336 //empty 337 break; 338 } 339 340 ICalPropertyScribe scribe = index.getPropertyScribe(property); 341 342 //marshal property 343 String value; 344 try { 345 value = scribe.writeText(property, context); 346 } catch (SkipMeException e) { 347 return; 348 } 349 350 //get parameters 351 ICalParameters parameters = scribe.prepareParameters(property, context); 352 353 /* 354 * Set the property's data type. 355 * 356 * Only add a VALUE parameter if the data type is: 357 * (1) not "unknown" 358 * (2) different from the property's default data type 359 */ 360 ICalDataType dataType = scribe.dataType(property, writer.getVersion()); 361 if (dataType != null && dataType != scribe.defaultDataType(writer.getVersion())) { 362 parameters = new ICalParameters(parameters); 363 parameters.setValue(dataType); 364 } 365 366 //get the property name 367 String propertyName; 368 if (writer.getVersion() == ICalVersion.V1_0 && property instanceof Created) { 369 //the vCal DCREATED property is the same as the iCal CREATED property 370 propertyName = "DCREATED"; 371 } else { 372 propertyName = scribe.getPropertyName(); 373 } 374 375 //write property to data stream 376 writer.writeProperty(propertyName, parameters, value); 377 } 378 379 /** 380 * Flushes the stream. 381 * @throws IOException if there's a problem flushing the stream 382 */ 383 public void flush() throws IOException { 384 writer.flush(); 385 } 386 387 /** 388 * Closes the underlying {@link Writer} object. 389 */ 390 public void close() throws IOException { 391 writer.close(); 392 } 393}