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}