001package biweekly.io.text; 002 003import static biweekly.util.StringUtils.NEWLINE; 004 005import java.io.Closeable; 006import java.io.IOException; 007import java.io.Reader; 008 009import biweekly.parameter.ICalParameters; 010 011/* 012 Copyright (c) 2013, Michael Angstadt 013 All rights reserved. 014 015 Redistribution and use in source and binary forms, with or without 016 modification, are permitted provided that the following conditions are met: 017 018 1. Redistributions of source code must retain the above copyright notice, this 019 list of conditions and the following disclaimer. 020 2. Redistributions in binary form must reproduce the above copyright notice, 021 this list of conditions and the following disclaimer in the documentation 022 and/or other materials provided with the distribution. 023 024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 027 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 028 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 029 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 030 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 031 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 032 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 033 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 034 */ 035 036/** 037 * Parses an iCalendar data stream. 038 * @author Michael Angstadt 039 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a> 040 */ 041public class ICalRawReader implements Closeable { 042 private final FoldedLineReader reader; 043 private boolean caretDecodingEnabled = true; 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 * Parses the next line of the iCalendar file. 063 * @return the next line or null if there are no more lines 064 * @throws ICalParseException if a line cannot be parsed 065 * @throws IOException if there's a problem reading from the input stream 066 */ 067 public ICalRawLine readLine() throws IOException { 068 String line = reader.readLine(); 069 if (line == null) { 070 return null; 071 } 072 073 String propertyName = null; 074 ICalParameters parameters = new ICalParameters(); 075 String value = null; 076 077 char escapeChar = 0; //is the next char escaped? 078 boolean inQuotes = false; //are we inside of double quotes? 079 StringBuilder buffer = new StringBuilder(); 080 String curParamName = null; 081 for (int i = 0; i < line.length(); i++) { 082 char ch = line.charAt(i); 083 084 if (escapeChar != 0) { 085 //this character was escaped 086 if (escapeChar == '\\') { 087 //backslash escaping in parameter values is not part of the standard 088 if (ch == '\\') { 089 buffer.append(ch); 090 } else if (ch == 'n' || ch == 'N') { 091 //newlines 092 buffer.append(NEWLINE); 093 } else if (ch == '"') { 094 //incase a double quote is escaped with a backslash 095 buffer.append(ch); 096 } else { 097 //treat the escape character as a normal character because it's not a valid escape sequence 098 buffer.append(escapeChar).append(ch); 099 } 100 } else if (escapeChar == '^') { 101 if (ch == '^') { 102 buffer.append(ch); 103 } else if (ch == 'n') { 104 buffer.append(NEWLINE); 105 } else if (ch == '\'') { 106 buffer.append('"'); 107 } else { 108 //treat the escape character as a normal character because it's not a valid escape sequence 109 buffer.append(escapeChar).append(ch); 110 } 111 } 112 escapeChar = 0; 113 continue; 114 } 115 116 if (ch == '\\' || (ch == '^' && caretDecodingEnabled)) { 117 //an escape character was read 118 escapeChar = ch; 119 continue; 120 } 121 122 if ((ch == ';' || ch == ':') && !inQuotes) { 123 if (propertyName == null) { 124 //property name 125 propertyName = buffer.toString(); 126 } else if (curParamName == null) { 127 //value-less parameter (bad iCal syntax) 128 String parameterName = buffer.toString(); 129 parameters.put(parameterName, null); 130 } else { 131 //parameter value 132 String paramValue = buffer.toString(); 133 parameters.put(curParamName, paramValue); 134 curParamName = null; 135 } 136 buffer.setLength(0); 137 138 if (ch == ':') { 139 //the rest of the line is the property value 140 if (i < line.length() - 1) { 141 value = line.substring(i + 1); 142 } else { 143 value = ""; 144 } 145 break; 146 } 147 continue; 148 } 149 150 if (ch == ',' && !inQuotes) { 151 //multi-valued parameter 152 parameters.put(curParamName, buffer.toString()); 153 buffer.setLength(0); 154 continue; 155 } 156 157 if (ch == '=' && curParamName == null) { 158 //parameter name 159 curParamName = buffer.toString(); 160 buffer.setLength(0); 161 continue; 162 } 163 164 if (ch == '"') { 165 inQuotes = !inQuotes; 166 continue; 167 } 168 169 buffer.append(ch); 170 } 171 172 if (propertyName == null || value == null) { 173 throw new ICalParseException(line); 174 } 175 176 return new ICalRawLine(propertyName, parameters, value); 177 } 178 179 /** 180 * <p> 181 * Gets whether the reader will decode parameter values that use circumflex 182 * accent encoding (enabled by default). This escaping mechanism allows 183 * newlines and double quotes to be included in parameter values. 184 * </p> 185 * 186 * <table border="1"> 187 * <tr> 188 * <th>Raw Character</th> 189 * <th>Encoded Character</th> 190 * </tr> 191 * <tr> 192 * <td>{@code "}</td> 193 * <td>{@code ^'}</td> 194 * </tr> 195 * <tr> 196 * <td><i>newline</i></td> 197 * <td>{@code ^n}</td> 198 * </tr> 199 * <tr> 200 * <td>{@code ^}</td> 201 * <td>{@code ^^}</td> 202 * </tr> 203 * </table> 204 * 205 * <p> 206 * Example: 207 * </p> 208 * 209 * <pre> 210 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt 211 * sburgh, PA 15212":40.446816;80.00566 212 * </pre> 213 * 214 * @return true if circumflex accent decoding is enabled, false if not 215 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 216 */ 217 public boolean isCaretDecodingEnabled() { 218 return caretDecodingEnabled; 219 } 220 221 /** 222 * <p> 223 * Sets whether the reader will decode parameter values that use circumflex 224 * accent encoding (enabled by default). This escaping mechanism allows 225 * newlines and double quotes to be included in parameter values. 226 * </p> 227 * 228 * <table border="1"> 229 * <tr> 230 * <th>Raw Character</th> 231 * <th>Encoded Character</th> 232 * </tr> 233 * <tr> 234 * <td>{@code "}</td> 235 * <td>{@code ^'}</td> 236 * </tr> 237 * <tr> 238 * <td><i>newline</i></td> 239 * <td>{@code ^n}</td> 240 * </tr> 241 * <tr> 242 * <td>{@code ^}</td> 243 * <td>{@code ^^}</td> 244 * </tr> 245 * </table> 246 * 247 * <p> 248 * Example: 249 * </p> 250 * 251 * <pre> 252 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt 253 * sburgh, PA 15212":geo:40.446816,-80.00566 254 * </pre> 255 * 256 * @param enable true to use circumflex accent decoding, false not to 257 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 258 */ 259 public void setCaretDecodingEnabled(boolean enable) { 260 caretDecodingEnabled = enable; 261 } 262 263 /** 264 * Closes the underlying {@link Reader} object. 265 */ 266 public void close() throws IOException { 267 reader.close(); 268 } 269}