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