    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 * &lt;CR&gt;&lt;LF&gt;
    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 * @version $Id: DotTerminatedMessageReader.java 1301276 2012-03-15 23:51:31Z sebb $
    039 */
    040public final class DotTerminatedMessageReader extends BufferedReader
    041{
    042    private static final char LF = '\n';
    043    private static final char CR = '\r';
    044    private static final int DOT = '.';
    045
    046    private boolean atBeginning;
    047    private boolean eof;
    048    private boolean seenCR; // was last character CR?
    049
    050    /**
    051     * Creates a DotTerminatedMessageReader that wraps an existing Reader
    052     * input source.
    053     * @param reader  The Reader input source containing the message.
    054     */
    055    public DotTerminatedMessageReader(Reader reader)
    056    {
    057        super(reader);
    058        // Assumes input is at start of message
    059        atBeginning = true;
    060        eof = false;
    061    }
    062
    063    /**
    064     * Reads and returns the next character in the message.  If the end of the
    065     * message has been reached, returns -1.  Note that a call to this method
    066     * may result in multiple reads from the underlying input stream to decode
    067     * the message properly (removing doubled dots and so on).  All of
    068     * this is transparent to the programmer and is only mentioned for
    069     * completeness.
    070     * @return The next character in the message. Returns -1 if the end of the
    071     *          message has been reached.
    072     * @exception IOException If an error occurs while reading the underlying
    073     *            stream.
    074     */
    075    @Override
    076    public int read() throws IOException {
    077        synchronized (lock) {
    078            if (eof) {
    079                return -1; // Don't allow read past EOF
    080            }
    081            int chint = super.read();
    082            if (chint == -1) { // True EOF
    083                eof = true;
    084                return -1;
    085            }
    086            if (atBeginning) {
    087                atBeginning = false;
    088                if (chint == DOT) { // Have DOT
    089                    mark(2); // need to check for CR LF or DOT
    090                    chint = super.read();
    091                    if (chint == -1) { // Should not happen
    092                        // new Throwable("Trailing DOT").printStackTrace();
    093                        eof = true;
    094                        return DOT; // return the trailing DOT
    095                    }
    096                    if (chint == DOT) { // Have DOT DOT
    097                        // no need to reset as we want to lose the first DOT
    098                        return chint; // i.e. DOT
    099                    }
    100                    if (chint == CR) { // Have DOT CR
    101                        chint = super.read();
    102                        if (chint == -1) { // Still only DOT CR - should not happen
    103                            //new Throwable("Trailing DOT CR").printStackTrace();
    104                            reset(); // So CR is picked up next time
    105                            return DOT; // return the trailing DOT
    106                        }
    107                        if (chint == LF) { // DOT CR LF
    108                            atBeginning = true;
    109                            eof = true;
    110                            // Do we need to clear the mark somehow?
    111                            return -1;
    112                        }
    113                    }
    114                    // Should not happen - lone DOT at beginning
    115                    //new Throwable("Lone DOT followed by "+(char)chint).printStackTrace();
    116                    reset();
    117                    return DOT;
    118                } // have DOT
    119            } // atBeginning
    120
    121            // Handle CRLF in normal flow
    122            if (seenCR) {
    123                seenCR = false;
    124                if (chint == LF) {
    125                    atBeginning = true;
    126                }
    127            }
    128            if (chint == CR) {
    129                seenCR = true;
    130            }
    131            return chint;
    132        }
    133    }
    134
    135
    136    /**
    137     * Reads the next characters from the message into an array and
    138     * returns the number of characters read.  Returns -1 if the end of the
    139     * message has been reached.
    140     * @param buffer  The character array in which to store the characters.
    141     * @return The number of characters read. Returns -1 if the
    142     *          end of the message has been reached.
    143     * @exception IOException If an error occurs in reading the underlying
    144     *            stream.
    145     */
    146    @Override
    147    public int read(char[] buffer) throws IOException
    148    {
    149        return read(buffer, 0, buffer.length);
    150    }
    151
    152    /**
    153     * Reads the next characters from the message into an array and
    154     * returns the number of characters read.  Returns -1 if the end of the
    155     * message has been reached.  The characters are stored in the array
    156     * starting from the given offset and up to the length specified.
    157     * @param buffer  The character array in which to store the characters.
    158     * @param offset   The offset into the array at which to start storing
    159     *              characters.
    160     * @param length   The number of characters to read.
    161     * @return The number of characters read. Returns -1 if the
    162     *          end of the message has been reached.
    163     * @exception IOException If an error occurs in reading the underlying
    164     *            stream.
    165     */
    166    @Override
    167    public int read(char[] buffer, int offset, int length) throws IOException
    168    {
    169        if (length < 1)
    170        {
    171            return 0;
    172        }
    173        int ch;
    174        synchronized (lock)
    175        {
    176            if ((ch = read()) == -1)
    177            {
    178                return -1;
    179            }
    180
    181            int off = offset;
    182
    183            do
    184            {
    185                buffer[offset++] = (char) ch;
    186            }
    187            while (--length > 0 && (ch = read()) != -1);
    188
    189            return (offset - off);
    190        }
    191    }
    192
    193    /**
    194     * Closes the message for reading.  This doesn't actually close the
    195     * underlying stream.  The underlying stream may still be used for
    196     * communicating with the server and therefore is not closed.
    197     * <p>
    198     * If the end of the message has not yet been reached, this method
    199     * will read the remainder of the message until it reaches the end,
    200     * so that the underlying stream may continue to be used properly
    201     * for communicating with the server.  If you do not fully read
    202     * a message, you MUST close it, otherwise your program will likely
    203     * hang or behave improperly.
    204     * @exception IOException  If an error occurs while reading the
    205     *            underlying stream.
    206     */
    207    @Override
    208    public void close() throws IOException
    209    {
    210        synchronized (lock)
    211        {
    212            if (!eof)
    213            {
    214                while (read() != -1)
    215                {
    216                    // read to EOF
    217                }
    218            }
    219            eof = true;
    220            atBeginning = false;
    221        }
    222    }
    223
    224    /**
    225     * Read a line of text.
    226     * A line is considered to be terminated by carriage return followed immediately by a linefeed.
    227     * This contrasts with BufferedReader which also allows other combinations.
    228     * @since 3.0
    229     */
    230    @Override
    231    public String readLine() throws IOException {
    232        StringBuilder sb = new StringBuilder();
    233        int intch;
    234        synchronized(lock) { // make thread-safe (hopefully!)
    235            while((intch = read()) != -1)
    236            {
    237                if (intch == LF && atBeginning) {
    238                    return sb.substring(0, sb.length()-1);
    239                }
    240                sb.append((char) intch);
    241            }
    242        }
    243        String string = sb.toString();
    244        if (string.length() == 0) { // immediate EOF
    245            return null;
    246        }
    247        // Should not happen - EOF without CRLF
    248        //new Throwable(string).printStackTrace();
    249        return string;
    250    }
    251}