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