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