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 *      http://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.IOException;
021import java.io.PushbackReader;
022import java.io.Reader;
023
024/**
025 * DotTerminatedMessageReader is a class used to read messages from a
026 * server that are terminated by a single dot followed by a
027 * <CR><LF>
028 * sequence and with double dots appearing at the begining of lines which
029 * do not signal end of message yet start with a dot.  Various Internet
030 * protocols such as NNTP and POP3 produce messages of this type.
031 * <p>
032 * This class handles stripping of the duplicate period at the beginning
033 * of lines starting with a period, converts NETASCII newlines to the
034 * local line separator format, truncates the end of message indicator,
035 * and ensures you cannot read past the end of the message.
036 * @author <a href="mailto:savarese@apache.org">Daniel F. Savarese</a>
037 * @version $Id: DotTerminatedMessageReader.java 636825 2008-03-13 18:34:52Z sebb $
038 */
039public final class DotTerminatedMessageReader extends Reader
040{
041    private static final String LS;
042    private static final char[] LS_CHARS;
043
044    static
045    {
046        LS = System.getProperty("line.separator");
047        LS_CHARS = LS.toCharArray();
048    }
049
050    private boolean atBeginning;
051    private boolean eof;
052    private int pos;
053    private char[] internalBuffer;
054    private PushbackReader internalReader;
055
056    /**
057     * Creates a DotTerminatedMessageReader that wraps an existing Reader
058     * input source.
059     * @param reader  The Reader input source containing the message.
060     */
061    public DotTerminatedMessageReader(Reader reader)
062    {
063        super(reader);
064        internalBuffer = new char[LS_CHARS.length + 3];
065        pos = internalBuffer.length;
066        // Assumes input is at start of message
067        atBeginning = true;
068        eof = false;
069        internalReader = new PushbackReader(reader);
070    }
071
072    /**
073     * Reads and returns the next character in the message.  If the end of the
074     * message has been reached, returns -1.  Note that a call to this method
075     * may result in multiple reads from the underlying input stream to decode
076     * the message properly (removing doubled dots and so on).  All of
077     * this is transparent to the programmer and is only mentioned for
078     * completeness.
079     * @return The next character in the message. Returns -1 if the end of the
080     *          message has been reached.
081     * @exception IOException If an error occurs while reading the underlying
082     *            stream.
083     */
084    @Override
085    public int read() throws IOException
086    {
087        int ch;
088
089        synchronized (lock)
090        {
091            if (pos < internalBuffer.length)
092            {
093                return internalBuffer[pos++];
094            }
095
096            if (eof)
097            {
098                return -1;
099            }
100
101            if ((ch = internalReader.read()) == -1)
102            {
103                eof = true;
104                return -1;
105            }
106
107            if (atBeginning)
108            {
109                atBeginning = false;
110                if (ch == '.')
111                {
112                    ch = internalReader.read();
113
114                    if (ch != '.')
115                    {
116                        // read newline
117                        eof = true;
118                        internalReader.read();
119                        return -1;
120                    }
121                    else
122                    {
123                        return '.';
124                    }
125                }
126            }
127
128            if (ch == '\r')
129            {
130                ch = internalReader.read();
131
132                if (ch == '\n')
133                {
134                    ch = internalReader.read();
135
136                    if (ch == '.')
137                    {
138                        ch = internalReader.read();
139
140                        if (ch != '.')
141                        {
142                            // read newline and indicate end of file
143                            internalReader.read();
144                            eof = true;
145                        }
146                        else
147                        {
148                            internalBuffer[--pos] = (char) ch;
149                        }
150                    }
151                    else
152                    {
153                        internalReader.unread(ch);
154                    }
155
156                    pos -= LS_CHARS.length;
157                    System.arraycopy(LS_CHARS, 0, internalBuffer, pos,
158                                     LS_CHARS.length);
159                    ch = internalBuffer[pos++];
160                }
161                else
162                {
163                    internalBuffer[--pos] = (char) ch;
164                    return '\r';
165                }
166            }
167
168            return ch;
169        }
170    }
171
172    /**
173     * Reads the next characters from the message into an array and
174     * returns the number of characters read.  Returns -1 if the end of the
175     * message has been reached.
176     * @param buffer  The character array in which to store the characters.
177     * @return The number of characters read. Returns -1 if the
178     *          end of the message has been reached.
179     * @exception IOException If an error occurs in reading the underlying
180     *            stream.
181     */
182    @Override
183    public int read(char[] buffer) throws IOException
184    {
185        return read(buffer, 0, buffer.length);
186    }
187
188    /**
189     * Reads the next characters from the message into an array and
190     * returns the number of characters read.  Returns -1 if the end of the
191     * message has been reached.  The characters are stored in the array
192     * starting from the given offset and up to the length specified.
193     * @param buffer  The character array in which to store the characters.
194     * @param offset   The offset into the array at which to start storing
195     *              characters.
196     * @param length   The number of characters to read.
197     * @return The number of characters read. Returns -1 if the
198     *          end of the message has been reached.
199     * @exception IOException If an error occurs in reading the underlying
200     *            stream.
201     */
202    @Override
203    public int read(char[] buffer, int offset, int length) throws IOException
204    {
205        int ch, off;
206        synchronized (lock)
207        {
208            if (length < 1)
209            {
210                return 0;
211            }
212            if ((ch = read()) == -1)
213            {
214                return -1;
215            }
216            off = offset;
217
218            do
219            {
220                buffer[offset++] = (char) ch;
221            }
222            while (--length > 0 && (ch = read()) != -1);
223
224            return (offset - off);
225        }
226    }
227
228    /**
229     * Determines if the message is ready to be read.
230     * @return True if the message is ready to be read, false if not.
231     * @exception IOException If an error occurs while checking the underlying
232     *            stream.
233     */
234    @Override
235    public boolean ready() throws IOException
236    {
237        synchronized (lock)
238        {
239            return (pos < internalBuffer.length || internalReader.ready());
240        }
241    }
242
243    /**
244     * Closes the message for reading.  This doesn't actually close the
245     * underlying stream.  The underlying stream may still be used for
246     * communicating with the server and therefore is not closed.
247     * <p>
248     * If the end of the message has not yet been reached, this method
249     * will read the remainder of the message until it reaches the end,
250     * so that the underlying stream may continue to be used properly
251     * for communicating with the server.  If you do not fully read
252     * a message, you MUST close it, otherwise your program will likely
253     * hang or behave improperly.
254     * @exception IOException  If an error occurs while reading the
255     *            underlying stream.
256     */
257    @Override
258    public void close() throws IOException
259    {
260        synchronized (lock)
261        {
262            if (internalReader == null)
263            {
264                return;
265            }
266
267            if (!eof)
268            {
269                while (read() != -1)
270                {
271                    ;
272                }
273            }
274            eof = true;
275            atBeginning = false;
276            pos = internalBuffer.length;
277            internalReader = null;
278        }
279    }
280}