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