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