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