    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}