View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.io;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.Reader;
23  
24  import org.apache.commons.net.util.NetConstants;
25  
26  /**
27   * 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
28   * 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
29   * NNTP and POP3 produce messages of this type.
30   * <p>
31   * 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
32   * message.
33   * </p>
34   * <p>
35   * 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.
36   * </p>
37   */
38  public final class DotTerminatedMessageReader extends BufferedReader {
39      private static final char LF = '\n';
40      private static final char CR = '\r';
41      private static final int DOT = '.';
42  
43      private boolean atBeginning;
44      private boolean eof;
45      private boolean seenCR; // was last character CR?
46  
47      /**
48       * Creates a DotTerminatedMessageReader that wraps an existing Reader input source.
49       *
50       * @param reader The Reader input source containing the message.
51       */
52      public DotTerminatedMessageReader(final Reader reader) {
53          super(reader);
54          // Assumes input is at start of message
55          atBeginning = true;
56          eof = false;
57      }
58  
59      /**
60       * Closes the message for reading. This doesn't actually close the underlying stream. The underlying stream may still be used for communicating with the
61       * server and therefore is not closed.
62       * <p>
63       * 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
64       * 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
65       * will likely hang or behave improperly.
66       * </p>
67       *
68       * @throws IOException If an error occurs while reading the underlying stream.
69       */
70      @Override
71      public void close() throws IOException {
72          synchronized (lock) {
73              if (!eof) {
74                  while (read() != -1) {
75                      // read to EOF
76                  }
77              }
78              eof = true;
79              atBeginning = false;
80          }
81      }
82  
83      /**
84       * 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
85       * 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
86       * programmer and is only mentioned for completeness.
87       *
88       * @return The next character in the message. Returns -1 if the end of the message has been reached.
89       * @throws IOException If an error occurs while reading the underlying stream.
90       */
91      @Override
92      public int read() throws IOException {
93          synchronized (lock) {
94              if (eof) {
95                  return NetConstants.EOS; // Don't allow read past EOF
96              }
97              int chint = super.read();
98              if (chint == NetConstants.EOS) { // True EOF
99                  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 }