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 }