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