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 <CR><LF> 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}