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