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