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 */
017package org.apache.commons.io.output;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.io.OutputStreamWriter;
023import java.io.Writer;
024import java.nio.charset.Charset;
025import java.nio.charset.CharsetEncoder;
026import java.nio.file.Files;
027import java.nio.file.StandardOpenOption;
028import java.util.Objects;
029
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.io.IOUtils;
032
033/**
034 * Writer of files that allows the encoding to be set.
035 * <p>
036 * This class provides a simple alternative to {@code FileWriter}
037 * that allows an encoding to be set. Unfortunately, it cannot subclass
038 * {@code FileWriter}.
039 * </p>
040 * <p>
041 * By default, the file will be overwritten, but this may be changed to append.
042 * </p>
043 * <p>
044 * The encoding must be specified using either the name of the {@link Charset},
045 * the {@link Charset}, or a {@link CharsetEncoder}. If the default encoding
046 * is required then use the {@link java.io.FileWriter} directly, rather than
047 * this implementation.
048 * </p>
049 *
050 * @since 1.4
051 */
052public class FileWriterWithEncoding extends Writer {
053    // Cannot extend ProxyWriter, as requires writer to be
054    // known when super() is called
055
056    /** The writer to decorate. */
057    private final Writer out;
058
059    /**
060     * Constructs a FileWriterWithEncoding with a file encoding.
061     *
062     * @param fileName  the name of the file to write to, not null
063     * @param charsetName  the name of the requested charset, not null
064     * @throws NullPointerException if the file name or encoding is null
065     * @throws IOException in case of an I/O error
066     */
067    public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
068        this(new File(fileName), charsetName, false);
069    }
070
071    /**
072     * Constructs a FileWriterWithEncoding with a file encoding.
073     *
074     * @param fileName  the name of the file to write to, not null
075     * @param charsetName  the name of the requested charset, not null
076     * @param append  true if content should be appended, false to overwrite
077     * @throws NullPointerException if the file name or encoding is null
078     * @throws IOException in case of an I/O error
079     */
080    public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append)
081            throws IOException {
082        this(new File(fileName), charsetName, append);
083    }
084
085    /**
086     * Constructs a FileWriterWithEncoding with a file encoding.
087     *
088     * @param fileName  the name of the file to write to, not null
089     * @param charset  the charset to use, not null
090     * @throws NullPointerException if the file name or encoding is null
091     * @throws IOException in case of an I/O error
092     */
093    public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
094        this(new File(fileName), charset, false);
095    }
096
097    /**
098     * Constructs a FileWriterWithEncoding with a file encoding.
099     *
100     * @param fileName  the name of the file to write to, not null
101     * @param charset  the encoding to use, not null
102     * @param append  true if content should be appended, false to overwrite
103     * @throws NullPointerException if the file name or encoding is null
104     * @throws IOException in case of an I/O error
105     */
106    public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append)
107            throws IOException {
108        this(new File(fileName), charset, append);
109    }
110
111    /**
112     * Constructs a FileWriterWithEncoding with a file encoding.
113     *
114     * @param fileName  the name of the file to write to, not null
115     * @param encoding  the encoding to use, not null
116     * @throws NullPointerException if the file name or encoding is null
117     * @throws IOException in case of an I/O error
118     */
119    public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
120        this(new File(fileName), encoding, false);
121    }
122
123    /**
124     * Constructs a FileWriterWithEncoding with a file encoding.
125     *
126     * @param fileName  the name of the file to write to, not null
127     * @param charsetEncoder  the encoding to use, not null
128     * @param append  true if content should be appended, false to overwrite
129     * @throws NullPointerException if the file name or encoding is null
130     * @throws IOException in case of an I/O error
131     */
132    public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append)
133            throws IOException {
134        this(new File(fileName), charsetEncoder, append);
135    }
136
137    /**
138     * Constructs a FileWriterWithEncoding with a file encoding.
139     *
140     * @param file  the file to write to, not null
141     * @param charsetName  the name of the requested charset, not null
142     * @throws NullPointerException if the file or encoding is null
143     * @throws IOException in case of an I/O error
144     */
145    public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
146        this(file, charsetName, false);
147    }
148
149    /**
150     * Constructs a FileWriterWithEncoding with a file encoding.
151     *
152     * @param file  the file to write to, not null
153     * @param charsetName  the name of the requested charset, not null
154     * @param append  true if content should be appended, false to overwrite
155     * @throws NullPointerException if the file or encoding is null
156     * @throws IOException in case of an I/O error
157     */
158    public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
159        this.out = initWriter(file, charsetName, append);
160    }
161
162    /**
163     * Constructs a FileWriterWithEncoding with a file encoding.
164     *
165     * @param file  the file to write to, not null
166     * @param charset  the encoding to use, not null
167     * @throws NullPointerException if the file or encoding is null
168     * @throws IOException in case of an I/O error
169     */
170    public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
171        this(file, charset, false);
172    }
173
174    /**
175     * Constructs a FileWriterWithEncoding with a file encoding.
176     *
177     * @param file  the file to write to, not null
178     * @param encoding  the name of the requested charset, not null
179     * @param append  true if content should be appended, false to overwrite
180     * @throws NullPointerException if the file or encoding is null
181     * @throws IOException in case of an I/O error
182     */
183    public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
184        this.out = initWriter(file, encoding, append);
185    }
186
187    /**
188     * Constructs a FileWriterWithEncoding with a file encoding.
189     *
190     * @param file  the file to write to, not null
191     * @param charsetEncoder  the encoding to use, not null
192     * @throws NullPointerException if the file or encoding is null
193     * @throws IOException in case of an I/O error
194     */
195    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
196        this(file, charsetEncoder, false);
197    }
198
199    /**
200     * Constructs a FileWriterWithEncoding with a file encoding.
201     *
202     * @param file  the file to write to, not null
203     * @param charsetEncoder  the encoding to use, not null
204     * @param append  true if content should be appended, false to overwrite
205     * @throws NullPointerException if the file or encoding is null
206     * @throws IOException in case of an I/O error
207     */
208    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append)
209            throws IOException {
210        this.out = initWriter(file, charsetEncoder, append);
211    }
212
213    /**
214     * Initialize the wrapped file writer.
215     * Ensure that a cleanup occurs if the writer creation fails.
216     *
217     * @param file  the file to be accessed
218     * @param encoding  the encoding to use - may be Charset, CharsetEncoder or String
219     * @param append  true to append
220     * @return the initialized writer
221     * @throws NullPointerException if the file or encoding is null
222     * @throws IOException if an error occurs
223     */
224     private static Writer initWriter(final File file, final Object encoding, final boolean append) throws IOException {
225        Objects.requireNonNull(file, "file");
226        Objects.requireNonNull(encoding, "encoding");
227        OutputStream stream = null;
228        final boolean fileExistedAlready = file.exists();
229        try {
230            stream = Files.newOutputStream(file.toPath(),  append ? StandardOpenOption.APPEND : StandardOpenOption.CREATE);
231            if (encoding instanceof Charset) {
232                return new OutputStreamWriter(stream, (Charset)encoding);
233            }
234            if (encoding instanceof CharsetEncoder) {
235                return new OutputStreamWriter(stream, (CharsetEncoder)encoding);
236            }
237            return new OutputStreamWriter(stream, (String)encoding);
238        } catch (final IOException | RuntimeException ex) {
239            try {
240                IOUtils.close(stream);
241            } catch (final IOException e) {
242                ex.addSuppressed(e);
243            }
244            if (!fileExistedAlready) {
245                FileUtils.deleteQuietly(file);
246            }
247            throw ex;
248        }
249    }
250
251    /**
252     * Write a character.
253     * @param idx the character to write
254     * @throws IOException if an I/O error occurs.
255     */
256     @Override
257    public void write(final int idx) throws IOException {
258        out.write(idx);
259    }
260
261    /**
262     * Write the characters from an array.
263     * @param chr the characters to write
264     * @throws IOException if an I/O error occurs.
265     */
266     @Override
267    public void write(final char[] chr) throws IOException {
268        out.write(chr);
269    }
270
271    /**
272     * Write the specified characters from an array.
273     * @param chr the characters to write
274     * @param st The start offset
275     * @param end The number of characters to write
276     * @throws IOException if an I/O error occurs.
277     */
278     @Override
279    public void write(final char[] chr, final int st, final int end) throws IOException {
280        out.write(chr, st, end);
281    }
282
283    /**
284     * Write the characters from a string.
285     * @param str the string to write
286     * @throws IOException if an I/O error occurs.
287     */
288     @Override
289    public void write(final String str) throws IOException {
290        out.write(str);
291    }
292
293    /**
294     * Write the specified characters from a string.
295     * @param str the string to write
296     * @param st The start offset
297     * @param end The number of characters to write
298     * @throws IOException if an I/O error occurs.
299     */
300     @Override
301    public void write(final String str, final int st, final int end) throws IOException {
302        out.write(str, st, end);
303    }
304
305    /**
306     * Flush the stream.
307     * @throws IOException if an I/O error occurs.
308     */
309     @Override
310    public void flush() throws IOException {
311        out.flush();
312    }
313
314    /**
315     * Close the stream.
316     * @throws IOException if an I/O error occurs.
317     */
318     @Override
319    public void close() throws IOException {
320        out.close();
321    }
322}