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