001 package biweekly.io.text; 002 003 import java.io.Closeable; 004 import java.io.IOException; 005 import java.io.Reader; 006 007 import biweekly.ICalException; 008 import biweekly.parameter.ICalParameters; 009 010 /* 011 Copyright (c) 2013, Michael Angstadt 012 All rights reserved. 013 014 Redistribution and use in source and binary forms, with or without 015 modification, are permitted provided that the following conditions are met: 016 017 1. Redistributions of source code must retain the above copyright notice, this 018 list of conditions and the following disclaimer. 019 2. Redistributions in binary form must reproduce the above copyright notice, 020 this list of conditions and the following disclaimer in the documentation 021 and/or other materials provided with the distribution. 022 023 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 024 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 025 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 026 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 027 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 028 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 029 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 030 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 031 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 032 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 033 */ 034 035 /** 036 * Parses an iCalendar data stream. 037 * @author Michael Angstadt 038 */ 039 public class ICalRawReader implements Closeable { 040 private static final String NEWLINE = System.getProperty("line.separator"); 041 private final FoldedLineReader reader; 042 private boolean caretDecodingEnabled = true; 043 private boolean eof = false; 044 045 /** 046 * Creates a new reader. 047 * @param reader the reader to the data stream 048 */ 049 public ICalRawReader(Reader reader) { 050 this.reader = new FoldedLineReader(reader); 051 } 052 053 /** 054 * Gets the line number of the last line that was read. 055 * @return the line number 056 */ 057 public int getLineNum() { 058 return reader.getLineNum(); 059 } 060 061 /** 062 * Starts or continues reading from the iCalendar data stream. 063 * @param listener handles the iCalendar data as it is read off the wire 064 * @throws IOException if there is an I/O problem 065 */ 066 public void start(ICalDataStreamListener listener) throws IOException { 067 String line; 068 while ((line = reader.readLine()) != null) { 069 try { 070 parseLine(line, listener); 071 } catch (StopReadingException e) { 072 return; 073 } 074 } 075 eof = true; 076 } 077 078 private void parseLine(String line, ICalDataStreamListener listener) { 079 String propertyName = null; 080 ICalParameters parameters = new ICalParameters(); 081 String value = null; 082 083 char escapeChar = 0; //is the next char escaped? 084 boolean inQuotes = false; //are we inside of double quotes? 085 StringBuilder buffer = new StringBuilder(); 086 String curParamName = null; 087 for (int i = 0; i < line.length(); i++) { 088 char ch = line.charAt(i); 089 if (escapeChar != 0) { 090 if (escapeChar == '\\') { 091 //backslash escaping in parameter values is not part of the standard 092 if (ch == '\\') { 093 buffer.append(ch); 094 } else if (ch == 'n' || ch == 'N') { 095 //newlines 096 buffer.append(NEWLINE); 097 } else if (ch == '"') { 098 //incase a double quote is escaped with a backslash 099 buffer.append(ch); 100 } else { 101 //treat the escape character as a normal character because it's not a valid escape sequence 102 buffer.append(escapeChar).append(ch); 103 } 104 } else if (escapeChar == '^') { 105 if (ch == '^') { 106 buffer.append(ch); 107 } else if (ch == 'n') { 108 buffer.append(NEWLINE); 109 } else if (ch == '\'') { 110 buffer.append('"'); 111 } else { 112 //treat the escape character as a normal character because it's not a valid escape sequence 113 buffer.append(escapeChar).append(ch); 114 } 115 } 116 escapeChar = 0; 117 } else if (ch == '\\' || (ch == '^' && caretDecodingEnabled)) { 118 escapeChar = ch; 119 } else if ((ch == ';' || ch == ':') && !inQuotes) { 120 if (propertyName == null) { 121 propertyName = buffer.toString(); 122 } else { 123 //parameter value 124 String paramValue = buffer.toString(); 125 parameters.put(curParamName, paramValue); 126 curParamName = null; 127 } 128 buffer.setLength(0); 129 130 if (ch == ':') { 131 if (i < line.length() - 1) { 132 value = line.substring(i + 1); 133 } else { 134 value = ""; 135 } 136 break; 137 } 138 } else if (ch == ',' && !inQuotes) { 139 //multi-valued parameter 140 parameters.put(curParamName, buffer.toString()); 141 buffer.setLength(0); 142 } else if (ch == '=' && curParamName == null) { 143 //parameter name 144 curParamName = buffer.toString(); 145 buffer.setLength(0); 146 } else if (ch == '"') { 147 inQuotes = !inQuotes; 148 } else { 149 buffer.append(ch); 150 } 151 } 152 153 if (propertyName == null || value == null) { 154 listener.invalidLine(line); 155 return; 156 } 157 if ("BEGIN".equalsIgnoreCase(propertyName)) { 158 listener.beginComponent(value); 159 return; 160 } 161 if ("END".equalsIgnoreCase(propertyName)) { 162 listener.endComponent(value); 163 return; 164 } 165 listener.readProperty(propertyName, parameters, value); 166 } 167 168 /** 169 * <p> 170 * Gets whether the reader will decode parameter values that use circumflex 171 * accent encoding (enabled by default). This escaping mechanism allows 172 * newlines and double quotes to be included in parameter values. 173 * </p> 174 * 175 * <table border="1"> 176 * <tr> 177 * <th>Raw Character</th> 178 * <th>Encoded Character</th> 179 * </tr> 180 * <tr> 181 * <td><code>"</code></td> 182 * <td><code>^'</code></td> 183 * </tr> 184 * <tr> 185 * <td><i>newline</i></td> 186 * <td><code>^n</code></td> 187 * </tr> 188 * <tr> 189 * <td><code>^</code></td> 190 * <td><code>^^</code></td> 191 * </tr> 192 * </table> 193 * 194 * <p> 195 * Example: 196 * </p> 197 * 198 * <pre> 199 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt 200 * sburgh, PA 15212":40.446816;80.00566 201 * </pre> 202 * 203 * @return true if circumflex accent decoding is enabled, false if not 204 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 205 */ 206 public boolean isCaretDecodingEnabled() { 207 return caretDecodingEnabled; 208 } 209 210 /** 211 * <p> 212 * Sets whether the reader will decode parameter values that use circumflex 213 * accent encoding (enabled by default). This escaping mechanism allows 214 * newlines and double quotes to be included in parameter values. 215 * </p> 216 * 217 * <table border="1"> 218 * <tr> 219 * <th>Raw Character</th> 220 * <th>Encoded Character</th> 221 * </tr> 222 * <tr> 223 * <td><code>"</code></td> 224 * <td><code>^'</code></td> 225 * </tr> 226 * <tr> 227 * <td><i>newline</i></td> 228 * <td><code>^n</code></td> 229 * </tr> 230 * <tr> 231 * <td><code>^</code></td> 232 * <td><code>^^</code></td> 233 * </tr> 234 * </table> 235 * 236 * <p> 237 * Example: 238 * </p> 239 * 240 * <pre> 241 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt 242 * sburgh, PA 15212":geo:40.446816,-80.00566 243 * </pre> 244 * 245 * @param enable true to use circumflex accent decoding, false not to 246 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 247 */ 248 public void setCaretDecodingEnabled(boolean enable) { 249 caretDecodingEnabled = enable; 250 } 251 252 /** 253 * Determines whether the end of the data stream has been reached. 254 * @return true if the end has been reached, false if not 255 */ 256 public boolean eof() { 257 return eof; 258 } 259 260 /** 261 * Handles the iCalendar data as it is read off the data stream. Each one of 262 * this interface's methods may throw a {@link StopReadingException} at any 263 * time to force the parser to stop reading from the data stream. This will 264 * cause the reader to return from the {@link ICalRawReader#start} method. 265 * To continue reading from the data stream, simply call the 266 * {@link ICalRawReader#start} method again. 267 * @author Michael Angstadt 268 */ 269 public static interface ICalDataStreamListener { 270 /** 271 * Called when a component begins (when a "BEGIN:NAME" property is 272 * reached). 273 * @param name the component name (e.g. "VEVENT") 274 * @throws StopReadingException to force the reader to stop reading from 275 * the data stream 276 */ 277 void beginComponent(String name); 278 279 /** 280 * Called when a property is read. 281 * @param name the property name (e.g. "VERSION") 282 * @param parameters the parameters 283 * @param value the property value 284 * @throws StopReadingException to force the reader to stop reading from 285 * the data stream 286 */ 287 void readProperty(String name, ICalParameters parameters, String value); 288 289 /** 290 * Called when a component ends (when a "END:NAME" property is reached). 291 * @param name the component name (e.g. "VEVENT") 292 * @throws StopReadingException to force the reader to stop reading from 293 * the data stream 294 */ 295 void endComponent(String name); 296 297 /** 298 * Called when a line cannot be parsed. 299 * @param line the unparseable line 300 * @throws StopReadingException to force the reader to stop reading from 301 * the data stream 302 */ 303 void invalidLine(String line); 304 } 305 306 /** 307 * Instructs an {@link ICalRawReader} to stop reading from the data stream 308 * when thrown from an {@link ICalDataStreamListener} implementation. 309 * @author Michael Angstadt 310 */ 311 @SuppressWarnings("serial") 312 public static class StopReadingException extends ICalException { 313 //empty 314 } 315 316 /** 317 * Closes the underlying {@link Reader} object. 318 */ 319 public void close() throws IOException { 320 reader.close(); 321 } 322 }