001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.io;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.Reader;
023
024/**
025 * DotTerminatedMessageReader is a class used to read messages from a
026 * server that are terminated by a single dot followed by a
027 * <CR><LF>
028 * sequence and with double dots appearing at the begining of lines which
029 * do not signal end of message yet start with a dot.  Various Internet
030 * protocols such as NNTP and POP3 produce messages of this type.
031 * <p>
032 * This class handles stripping of the duplicate period at the beginning
033 * of lines starting with a period, and ensures you cannot read past the end of the message.
034 * <p>
035 * Note: versions since 3.0 extend BufferedReader rather than Reader,
036 * and no longer change the CRLF into the local EOL. Also only DOT CR LF
037 * acts as EOF.
038 */
039public final class DotTerminatedMessageReader extends BufferedReader
040{
041    private static final char LF = '\n';
042    private static final char CR = '\r';
043    private static final int DOT = '.';
044
045    private boolean atBeginning;
046    private boolean eof;
047    private boolean seenCR; // was last character CR?
048
049    /**
050     * Creates a DotTerminatedMessageReader that wraps an existing Reader
051     * input source.
052     * @param reader  The Reader input source containing the message.
053     */
054    public DotTerminatedMessageReader(final Reader reader)
055    {
056        super(reader);
057        // Assumes input is at start of message
058        atBeginning = true;
059        eof = false;
060    }
061
062    /**
063     * Reads and returns the next character in the message.  If the end of the
064     * message has been reached, returns -1.  Note that a call to this method
065     * may result in multiple reads from the underlying input stream to decode
066     * the message properly (removing doubled dots and so on).  All of
067     * this is transparent to the programmer and is only mentioned for
068     * completeness.
069     * @return The next character in the message. Returns -1 if the end of the
070     *          message has been reached.
071     * @throws IOException If an error occurs while reading the underlying
072     *            stream.
073     */
074    @Override
075    public int read() throws IOException {
076        synchronized (lock) {
077            if (eof) {
078                return -1; // Don't allow read past EOF
079            }
080            int chint = super.read();
081            if (chint == -1) { // True EOF
082                eof = true;
083                return -1;
084            }
085            if (atBeginning) {
086                atBeginning = false;
087                if (chint == DOT) { // Have DOT
088                    mark(2); // need to check for CR LF or DOT
089                    chint = super.read();
090                    if (chint == -1) { // Should not happen
091                        // new Throwable("Trailing DOT").printStackTrace();
092                        eof = true;
093                        return DOT; // return the trailing DOT
094                    }
095                    if (chint == DOT) { // Have DOT DOT
096                        // no need to reset as we want to lose the first DOT
097                        return chint; // i.e. DOT
098                    }
099                    if (chint == CR) { // Have DOT CR
100                        chint = super.read();
101                        if (chint == -1) { // Still only DOT CR - should not happen
102                            //new Throwable("Trailing DOT CR").printStackTrace();
103                            reset(); // So CR is picked up next time
104                            return DOT; // return the trailing DOT
105                        }
106                        if (chint == LF) { // DOT CR LF
107                            atBeginning = true;
108                            eof = true;
109                            // Do we need to clear the mark somehow?
110                            return -1;
111                        }
112                    }
113                    // Should not happen - lone DOT at beginning
114                    //new Throwable("Lone DOT followed by "+(char)chint).printStackTrace();
115                    reset();
116                    return DOT;
117                } // have DOT
118            } // atBeginning
119
120            // Handle CRLF in normal flow
121            if (seenCR) {
122                seenCR = false;
123                if (chint == LF) {
124                    atBeginning = true;
125                }
126            }
127            if (chint == CR) {
128                seenCR = true;
129            }
130            return chint;
131        }
132    }
133
134
135    /**
136     * Reads the next characters from the message into an array and
137     * returns the number of characters read.  Returns -1 if the end of the
138     * message has been reached.
139     * @param buffer  The character array in which to store the characters.
140     * @return The number of characters read. Returns -1 if the
141     *          end of the message has been reached.
142     * @throws IOException If an error occurs in reading the underlying
143     *            stream.
144     */
145    @Override
146    public int read(final char[] buffer) throws IOException
147    {
148        return read(buffer, 0, buffer.length);
149    }
150
151    /**
152     * Reads the next characters from the message into an array and
153     * returns the number of characters read.  Returns -1 if the end of the
154     * message has been reached.  The characters are stored in the array
155     * starting from the given offset and up to the length specified.
156     * @param buffer  The character array in which to store the characters.
157     * @param offset   The offset into the array at which to start storing
158     *              characters.
159     * @param length   The number of characters to read.
160     * @return The number of characters read. Returns -1 if the
161     *          end of the message has been reached.
162     * @throws IOException If an error occurs in reading the underlying
163     *            stream.
164     */
165    @Override
166    public int read(final char[] buffer, int offset, int length) throws IOException
167    {
168        if (length < 1)
169        {
170            return 0;
171        }
172        int ch;
173        synchronized (lock)
174        {
175            if ((ch = read()) == -1)
176            {
177                return -1;
178            }
179
180            final int off = offset;
181
182            do
183            {
184                buffer[offset++] = (char) ch;
185            }
186            while (--length > 0 && (ch = read()) != -1);
187
188            return offset - off;
189        }
190    }
191
192    /**
193     * Closes the message for reading.  This doesn't actually close the
194     * underlying stream.  The underlying stream may still be used for
195     * communicating with the server and therefore is not closed.
196     * <p>
197     * If the end of the message has not yet been reached, this method
198     * will read the remainder of the message until it reaches the end,
199     * so that the underlying stream may continue to be used properly
200     * for communicating with the server.  If you do not fully read
201     * a message, you MUST close it, otherwise your program will likely
202     * hang or behave improperly.
203     * @throws IOException  If an error occurs while reading the
204     *            underlying stream.
205     */
206    @Override
207    public void close() throws IOException
208    {
209        synchronized (lock)
210        {
211            if (!eof)
212            {
213                while (read() != -1)
214                {
215                    // read to EOF
216                }
217            }
218            eof = true;
219            atBeginning = false;
220        }
221    }
222
223    /**
224     * Read a line of text.
225     * A line is considered to be terminated by carriage return followed immediately by a linefeed.
226     * This contrasts with BufferedReader which also allows other combinations.
227     * @since 3.0
228     */
229    @Override
230    public String readLine() throws IOException {
231        final StringBuilder sb = new StringBuilder();
232        int intch;
233        synchronized(lock) { // make thread-safe (hopefully!)
234            while((intch = read()) != -1)
235            {
236                if (intch == LF && atBeginning) {
237                    return sb.substring(0, sb.length()-1);
238                }
239                sb.append((char) intch);
240            }
241        }
242        final String string = sb.toString();
243        if (string.isEmpty()) { // immediate EOF
244            return null;
245        }
246        // Should not happen - EOF without CRLF
247        //new Throwable(string).printStackTrace();
248        return string;
249    }
250}