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