    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.FileWriter;
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.io.OutputStreamWriter;
    024import java.nio.charset.Charset;
    025import java.nio.charset.CharsetEncoder;
    026import java.util.Objects;
    027
    028import org.apache.commons.io.Charsets;
    029import org.apache.commons.io.FileUtils;
    030import org.apache.commons.io.IOUtils;
    031import org.apache.commons.io.build.AbstractStreamBuilder;
    032
    033/**
    034 * Writer of files that allows the encoding to be set.
    035 * <p>
    036 * This class provides a simple alternative to {@link FileWriter} that allows an encoding to be set. Unfortunately, it cannot subclass {@link FileWriter}.
    037 * </p>
    038 * <p>
    039 * By default, the file will be overwritten, but this may be changed to append.
    040 * </p>
    041 * <p>
    042 * The encoding must be specified using either the name of the {@link Charset}, the {@link Charset}, or a {@link CharsetEncoder}. If the default encoding is
    043 * required then use the {@link java.io.FileWriter} directly, rather than this implementation.
    044 * </p>
    045 *
    046 * @since 1.4
    047 */
    048public class FileWriterWithEncoding extends ProxyWriter {
    049
    050    /**
    051     * Builds a new {@link FileWriterWithEncoding} instance.
    052     * <p>
    053     * Using a CharsetEncoder:
    054     * </p>
    055     * <pre>{@code
    056     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
    057     *   .setPath(path)
    058     *   .setAppend(false)
    059     *   .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder())
    060     *   .get()}
    061     * </pre>
    062     * <p>
    063     * Using a Charset:
    064     * </p>
    065     * <pre>{@code
    066     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
    067     *   .setPath(path)
    068     *   .setAppend(false)
    069     *   .setCharsetEncoder(StandardCharsets.UTF_8)
    070     *   .get()}
    071     * </pre>
    072     * @since 2.12.0
    073     */
    074    public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> {
    075
    076        private boolean append;
    077
    078        private CharsetEncoder charsetEncoder = super.getCharset().newEncoder();
    079
    080        /**
    081         * Constructs a new instance.
    082         *
    083         * @throws UnsupportedOperationException if the origin cannot be converted to a File.
    084         */
    085        @SuppressWarnings("resource")
    086        @Override
    087        public FileWriterWithEncoding get() throws IOException {
    088            if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) {
    089                throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset()));
    090            }
    091            final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
    092            return new FileWriterWithEncoding(FileWriterWithEncoding.initWriter(getOrigin().getFile(), encoder, append));
    093        }
    094
    095        /**
    096         * Sets whether or not to append.
    097         *
    098         * @param append Whether or not to append.
    099         * @return this
    100         */
    101        public Builder setAppend(final boolean append) {
    102            this.append = append;
    103            return this;
    104        }
    105
    106        /**
    107         * Sets charsetEncoder to use for encoding.
    108         *
    109         * @param charsetEncoder The charsetEncoder to use for encoding.
    110         * @return this
    111         */
    112        public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
    113            this.charsetEncoder = charsetEncoder;
    114            return this;
    115        }
    116
    117    }
    118
    119    /**
    120     * Constructs a new {@link Builder}.
    121     *
    122     * @return Creates a new {@link Builder}.
    123     * @since 2.12.0
    124     */
    125    public static Builder builder() {
    126        return new Builder();
    127    }
    128
    129    /**
    130     * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
    131     *
    132     * @param file     the file to be accessed
    133     * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
    134     * @param append   true to append
    135     * @return a new initialized OutputStreamWriter
    136     * @throws IOException if an error occurs
    137     */
    138    private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
    139        Objects.requireNonNull(file, "file");
    140        OutputStream outputStream = null;
    141        final boolean fileExistedAlready = file.exists();
    142        try {
    143            outputStream = FileUtils.newOutputStream(file, append);
    144            if (encoding == null || encoding instanceof Charset) {
    145                return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
    146            }
    147            if (encoding instanceof CharsetEncoder) {
    148                return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
    149            }
    150            return new OutputStreamWriter(outputStream, (String) encoding);
    151        } catch (final IOException | RuntimeException ex) {
    152            try {
    153                IOUtils.close(outputStream);
    154            } catch (final IOException e) {
    155                ex.addSuppressed(e);
    156            }
    157            if (!fileExistedAlready) {
    158                FileUtils.deleteQuietly(file);
    159            }
    160            throw ex;
    161        }
    162    }
    163
    164    /**
    165     * Constructs a FileWriterWithEncoding with a file encoding.
    166     *
    167     * @param file    the file to write to, not null
    168     * @param charset the encoding to use, not null
    169     * @throws NullPointerException if the file or encoding is null
    170     * @throws IOException          in case of an I/O error
    171     * @deprecated Use {@link #builder()}
    172     */
    173    @Deprecated
    174    public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
    175        this(file, charset, false);
    176    }
    177
    178    /**
    179     * Constructs a FileWriterWithEncoding with a file encoding.
    180     *
    181     * @param file     the file to write to, not null.
    182     * @param encoding the name of the requested charset, null uses the default Charset.
    183     * @param append   true if content should be appended, false to overwrite.
    184     * @throws NullPointerException if the file is null.
    185     * @throws IOException          in case of an I/O error.
    186     * @deprecated Use {@link #builder()}
    187     */
    188    @Deprecated
    189    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
    190    public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
    191        this(initWriter(file, encoding, append));
    192    }
    193
    194    /**
    195     * Constructs a FileWriterWithEncoding with a file encoding.
    196     *
    197     * @param file           the file to write to, not null
    198     * @param charsetEncoder the encoding to use, not null
    199     * @throws NullPointerException if the file or encoding is null
    200     * @throws IOException          in case of an I/O error
    201     * @deprecated Use {@link #builder()}
    202     */
    203    @Deprecated
    204    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
    205        this(file, charsetEncoder, false);
    206    }
    207
    208    /**
    209     * Constructs a FileWriterWithEncoding with a file encoding.
    210     *
    211     * @param file           the file to write to, not null.
    212     * @param charsetEncoder the encoding to use, null uses the default Charset.
    213     * @param append         true if content should be appended, false to overwrite.
    214     * @throws NullPointerException if the file is null.
    215     * @throws IOException          in case of an I/O error.
    216     * @deprecated Use {@link #builder()}
    217     */
    218    @Deprecated
    219    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
    220    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
    221        this(initWriter(file, charsetEncoder, append));
    222    }
    223
    224    /**
    225     * Constructs a FileWriterWithEncoding with a file encoding.
    226     *
    227     * @param file        the file to write to, not null
    228     * @param charsetName the name of the requested charset, not null
    229     * @throws NullPointerException if the file or encoding is null
    230     * @throws IOException          in case of an I/O error
    231     * @deprecated Use {@link #builder()}
    232     */
    233    @Deprecated
    234    public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
    235        this(file, charsetName, false);
    236    }
    237
    238    /**
    239     * Constructs a FileWriterWithEncoding with a file encoding.
    240     *
    241     * @param file        the file to write to, not null.
    242     * @param charsetName the name of the requested charset, null uses the default Charset.
    243     * @param append      true if content should be appended, false to overwrite.
    244     * @throws NullPointerException if the file is null.
    245     * @throws IOException          in case of an I/O error.
    246     * @deprecated Use {@link #builder()}
    247     */
    248    @Deprecated
    249    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
    250    public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
    251        this(initWriter(file, charsetName, append));
    252    }
    253
    254    private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
    255        super(outputStreamWriter);
    256    }
    257
    258    /**
    259     * Constructs a FileWriterWithEncoding with a file encoding.
    260     *
    261     * @param fileName the name of the file to write to, not null
    262     * @param charset  the charset to use, not null
    263     * @throws NullPointerException if the file name or encoding is null
    264     * @throws IOException          in case of an I/O error
    265     * @deprecated Use {@link #builder()}
    266     */
    267    @Deprecated
    268    public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
    269        this(new File(fileName), charset, false);
    270    }
    271
    272    /**
    273     * Constructs a FileWriterWithEncoding with a file encoding.
    274     *
    275     * @param fileName the name of the file to write to, not null
    276     * @param charset  the encoding to use, not null
    277     * @param append   true if content should be appended, false to overwrite
    278     * @throws NullPointerException if the file name or encoding is null
    279     * @throws IOException          in case of an I/O error
    280     * @deprecated Use {@link #builder()}
    281     */
    282    @Deprecated
    283    public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
    284        this(new File(fileName), charset, append);
    285    }
    286
    287    /**
    288     * Constructs a FileWriterWithEncoding with a file encoding.
    289     *
    290     * @param fileName the name of the file to write to, not null
    291     * @param encoding the encoding to use, not null
    292     * @throws NullPointerException if the file name or encoding is null
    293     * @throws IOException          in case of an I/O error
    294     * @deprecated Use {@link #builder()}
    295     */
    296    @Deprecated
    297    public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
    298        this(new File(fileName), encoding, false);
    299    }
    300
    301    /**
    302     * Constructs a FileWriterWithEncoding with a file encoding.
    303     *
    304     * @param fileName       the name of the file to write to, not null
    305     * @param charsetEncoder the encoding to use, not null
    306     * @param append         true if content should be appended, false to overwrite
    307     * @throws NullPointerException if the file name or encoding is null
    308     * @throws IOException          in case of an I/O error
    309     * @deprecated Use {@link #builder()}
    310     */
    311    @Deprecated
    312    public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
    313        this(new File(fileName), charsetEncoder, append);
    314    }
    315
    316    /**
    317     * Constructs a FileWriterWithEncoding with a file encoding.
    318     *
    319     * @param fileName    the name of the file to write to, not null
    320     * @param charsetName the name of the requested charset, not null
    321     * @throws NullPointerException if the file name or encoding is null
    322     * @throws IOException          in case of an I/O error
    323     * @deprecated Use {@link #builder()}
    324     */
    325    @Deprecated
    326    public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
    327        this(new File(fileName), charsetName, false);
    328    }
    329
    330    /**
    331     * Constructs a FileWriterWithEncoding with a file encoding.
    332     *
    333     * @param fileName    the name of the file to write to, not null
    334     * @param charsetName the name of the requested charset, not null
    335     * @param append      true if content should be appended, false to overwrite
    336     * @throws NullPointerException if the file name or encoding is null
    337     * @throws IOException          in case of an I/O error
    338     * @deprecated Use {@link #builder()}
    339     */
    340    @Deprecated
    341    public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
    342        this(new File(fileName), charsetName, append);
    343    }
    344}