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