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