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.Writer;
022
023/**
024 * DotTerminatedMessageWriter is a class used to write messages to a
025 * server that are terminated by a single dot followed by a
026 * <CR><LF>
027 * sequence and with double dots appearing at the begining of lines which
028 * do not signal end of message yet start with a dot.  Various Internet
029 * protocols such as NNTP and POP3 produce messages of this type.
030 * <p>
031 * This class handles the doubling of line-starting periods,
032 * converts single linefeeds to NETASCII newlines, and on closing
033 * will send the final message terminator dot and NETASCII newline
034 * sequence.
035 *
036 *
037 */
038
039public final class DotTerminatedMessageWriter extends Writer
040{
041    private static final int NOTHING_SPECIAL_STATE = 0;
042    private static final int LAST_WAS_CR_STATE = 1;
043    private static final int LAST_WAS_NL_STATE = 2;
044
045    private int state;
046    private Writer output;
047
048
049    /**
050     * Creates a DotTerminatedMessageWriter that wraps an existing Writer
051     * output destination.
052     *
053     * @param output  The Writer output destination to write the message.
054     */
055    public DotTerminatedMessageWriter(final Writer output)
056    {
057        super(output);
058        this.output = output;
059        this.state = NOTHING_SPECIAL_STATE;
060    }
061
062
063    /**
064     * Writes a character to the output.  Note that a call to this method
065     * may result in multiple writes to the underling Writer in order to
066     * convert naked linefeeds to NETASCII line separators and to double
067     * line-leading periods.  This is transparent to the programmer and
068     * is only mentioned for completeness.
069     *
070     * @param ch  The character to write.
071     * @throws IOException  If an error occurs while writing to the
072     *            underlying output.
073     */
074    @Override
075    public void write(final int ch) throws IOException
076    {
077        synchronized (lock)
078        {
079            switch (ch)
080            {
081            case '\r':
082                state = LAST_WAS_CR_STATE;
083                output.write('\r');
084                return ;
085            case '\n':
086                if (state != LAST_WAS_CR_STATE) {
087                    output.write('\r');
088                }
089                output.write('\n');
090                state = LAST_WAS_NL_STATE;
091                return ;
092            case '.':
093                // Double the dot at the beginning of a line
094                if (state == LAST_WAS_NL_STATE) {
095                    output.write('.');
096                }
097                //$FALL-THROUGH$
098            default:
099                state = NOTHING_SPECIAL_STATE;
100                output.write(ch);
101            }
102        }
103    }
104
105
106    /**
107     * Writes a number of characters from a character array to the output
108     * starting from a given offset.
109     *
110     * @param buffer  The character array to write.
111     * @param offset  The offset into the array at which to start copying data.
112     * @param length  The number of characters to write.
113     * @throws IOException If an error occurs while writing to the underlying
114     *            output.
115     */
116    @Override
117    public void write(final char[] buffer, int offset, int length) throws IOException
118    {
119        synchronized (lock)
120        {
121            while (length-- > 0) {
122                write(buffer[offset++]);
123            }
124        }
125    }
126
127
128    /**
129     * Writes a character array to the output.
130     *
131     * @param buffer  The character array to write.
132     * @throws IOException If an error occurs while writing to the underlying
133     *            output.
134     */
135    @Override
136    public void write(final char[] buffer) throws IOException
137    {
138        write(buffer, 0, buffer.length);
139    }
140
141
142    /**
143     * Writes a String to the output.
144     *
145     * @param string  The String to write.
146     * @throws IOException If an error occurs while writing to the underlying
147     *            output.
148     */
149    @Override
150    public void write(final String string) throws IOException
151    {
152        write(string.toCharArray());
153    }
154
155
156    /**
157     * Writes part of a String to the output starting from a given offset.
158     *
159     * @param string  The String to write.
160     * @param offset  The offset into the String at which to start copying data.
161     * @param length  The number of characters to write.
162     * @throws IOException If an error occurs while writing to the underlying
163     *            output.
164     */
165    @Override
166    public void write(final String string, final int offset, final int length) throws IOException
167    {
168        write(string.toCharArray(), offset, length);
169    }
170
171
172    /**
173     * Flushes the underlying output, writing all buffered output.
174     *
175     * @throws IOException If an error occurs while writing to the underlying
176     *            output.
177     */
178    @Override
179    public void flush() throws IOException
180    {
181        synchronized (lock)
182        {
183            output.flush();
184        }
185    }
186
187
188    /**
189     * Flushes the underlying output, writing all buffered output, but doesn't
190     * actually close the underlying stream.  The underlying stream may still
191     * be used for communicating with the server and therefore is not closed.
192     *
193     * @throws IOException If an error occurs while writing to the underlying
194     *            output or closing the Writer.
195     */
196    @Override
197    public void close() throws IOException
198    {
199        synchronized (lock)
200        {
201            if (output == null) {
202                return ;
203            }
204
205            if (state == LAST_WAS_CR_STATE) {
206                output.write('\n');
207            } else if (state != LAST_WAS_NL_STATE) {
208                output.write("\r\n");
209            }
210
211            output.write(".\r\n");
212
213            output.flush();
214            output = null;
215        }
216    }
217
218}