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 636825 2008-03-13 18:34:52Z sebb $
038     */
039    public 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    }