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}