blob: 5db985aba9096dcbafc3c6ffd7aaf3d41c50e07f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.net.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
/**
* 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 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 produce messages of this type.
* <p>
* 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 message.
* <p>
* 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.
*/
public final class DotTerminatedMessageReader extends BufferedReader
{
private static final char LF = '\n';
private static final char CR = '\r';
private static final int DOT = '.';
private boolean atBeginning;
private boolean eof;
private boolean seenCR; // was last character CR?
/**
* Creates a DotTerminatedMessageReader that wraps an existing Reader
* input source.
* @param reader The Reader input source containing the message.
*/
public DotTerminatedMessageReader(Reader reader)
{
super(reader);
// Assumes input is at start of message
atBeginning = true;
eof = false;
}
/**
* 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 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 programmer and is only mentioned for
* completeness.
* @return The next character in the message. Returns -1 if the end of the
* message has been reached.
* @throws IOException If an error occurs while reading the underlying
* stream.
*/
@Override
public int read() throws IOException {
synchronized (lock) {
if (eof) {
return -1; // Don't allow read past EOF
}
int chint = super.read();
if (chint == -1) { // True EOF
eof = true;
return -1;
}
if (atBeginning) {
atBeginning = false;
if (chint == DOT) { // Have DOT
mark(2); // need to check for CR LF or DOT
chint = super.read();
if (chint == -1) { // Should not happen
// new Throwable("Trailing DOT").printStackTrace();
eof = true;
return DOT; // return the trailing DOT
}
if (chint == DOT) { // Have DOT DOT
// no need to reset as we want to lose the first DOT
return chint; // i.e. DOT
}
if (chint == CR) { // Have DOT CR
chint = super.read();
if (chint == -1) { // Still only DOT CR - should not happen
//new Throwable("Trailing DOT CR").printStackTrace();
reset(); // So CR is picked up next time
return DOT; // return the trailing DOT
}
if (chint == LF) { // DOT CR LF
atBeginning = true;
eof = true;
// Do we need to clear the mark somehow?
return -1;
}
}
// Should not happen - lone DOT at beginning
//new Throwable("Lone DOT followed by "+(char)chint).printStackTrace();
reset();
return DOT;
} // have DOT
} // atBeginning
// Handle CRLF in normal flow
if (seenCR) {
seenCR = false;
if (chint == LF) {
atBeginning = true;
}
}
if (chint == CR) {
seenCR = true;
}
return chint;
}
}
/**
* 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 reached.
* @param buffer The character array in which to store the characters.
* @return The number of characters read. Returns -1 if the
* end of the message has been reached.
* @throws IOException If an error occurs in reading the underlying
* stream.
*/
@Override
public int read(char[] buffer) throws IOException
{
return read(buffer, 0, buffer.length);
}
/**
* 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 reached. The characters are stored in the array
* starting from the given offset and up to the length specified.
* @param buffer The character array in which to store the characters.
* @param offset The offset into the array at which to start storing
* characters.
* @param length The number of characters to read.
* @return The number of characters read. Returns -1 if the
* end of the message has been reached.
* @throws IOException If an error occurs in reading the underlying
* stream.
*/
@Override
public int read(char[] buffer, int offset, int length) throws IOException
{
if (length < 1)
{
return 0;
}
int ch;
synchronized (lock)
{
if ((ch = read()) == -1)
{
return -1;
}
int off = offset;
do
{
buffer[offset++] = (char) ch;
}
while (--length > 0 && (ch = read()) != -1);
return (offset - off);
}
}
/**
* Closes the message for reading. This doesn't actually close the
* underlying stream. The underlying stream may still be used for
* communicating with the server and therefore is not closed.
* <p>
* 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 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 will likely
* hang or behave improperly.
* @throws IOException If an error occurs while reading the
* underlying stream.
*/
@Override
public void close() throws IOException
{
synchronized (lock)
{
if (!eof)
{
while (read() != -1)
{
// read to EOF
}
}
eof = true;
atBeginning = false;
}
}
/**
* Read a line of text.
* A line is considered to be terminated by carriage return followed immediately by a linefeed.
* This contrasts with BufferedReader which also allows other combinations.
* @since 3.0
*/
@Override
public String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int intch;
synchronized(lock) { // make thread-safe (hopefully!)
while((intch = read()) != -1)
{
if (intch == LF && atBeginning) {
return sb.substring(0, sb.length()-1);
}
sb.append((char) intch);
}
}
String string = sb.toString();
if (string.length() == 0) { // immediate EOF
return null;
}
// Should not happen - EOF without CRLF
//new Throwable(string).printStackTrace();
return string;
}
}