    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 * &lt;CR&gt;&lt;LF&gt;
    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}