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}