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 <CR><LF> 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}