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            try {
192                IOUtils.close(outputStream);
193            } catch (final IOException e) {
194                ex.addSuppressed(e);
195            }
196            if (!fileExistedAlready) {
197                FileUtils.deleteQuietly(file);
198            }
199            throw ex;
200        }
201    }
202
203    @SuppressWarnings("resource") // caller closes
204    private FileWriterWithEncoding(final Builder builder) throws IOException {
205        super(initWriter(builder.checkOriginFile(), builder.getEncoder(), builder.append));
206    }
207
208    /**
209     * Constructs a FileWriterWithEncoding with a file encoding.
210     *
211     * @param file    the file to write to, not null.
212     * @param charset the encoding to use, not null.
213     * @throws NullPointerException if the file or encoding is null.
214     * @throws IOException          in case of an I/O error.
215     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
216     */
217    @Deprecated
218    public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
219        this(file, charset, false);
220    }
221
222    /**
223     * Constructs a FileWriterWithEncoding with a file encoding.
224     *
225     * @param file     the file to write to, not null.
226     * @param encoding the name of the requested charset, null uses the default Charset.
227     * @param append   true if content should be appended, false to overwrite.
228     * @throws NullPointerException if the file is null.
229     * @throws IOException          in case of an I/O error.
230     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
231     */
232    @Deprecated
233    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
234    public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
235        this(initWriter(file, encoding, append));
236    }
237
238    /**
239     * Constructs a FileWriterWithEncoding with a file encoding.
240     *
241     * @param file           the file to write to, not null.
242     * @param charsetEncoder the encoding to use, not null.
243     * @throws NullPointerException if the file or encoding is null.
244     * @throws IOException          in case of an I/O error.
245     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
246     */
247    @Deprecated
248    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
249        this(file, charsetEncoder, false);
250    }
251
252    /**
253     * Constructs a FileWriterWithEncoding with a file encoding.
254     *
255     * @param file           the file to write to, not null.
256     * @param charsetEncoder the encoding to use, null uses the default Charset.
257     * @param append         true if content should be appended, false to overwrite.
258     * @throws NullPointerException if the file is null.
259     * @throws IOException          in case of an I/O error.
260     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
261     */
262    @Deprecated
263    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
264    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
265        this(initWriter(file, charsetEncoder, append));
266    }
267
268    /**
269     * Constructs a FileWriterWithEncoding with a file encoding.
270     *
271     * @param file        the file to write to, not null.
272     * @param charsetName the name of the requested charset, not null.
273     * @throws NullPointerException if the file or encoding is null.
274     * @throws IOException          in case of an I/O error.
275     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
276     */
277    @Deprecated
278    public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
279        this(file, charsetName, false);
280    }
281
282    /**
283     * Constructs a FileWriterWithEncoding with a file encoding.
284     *
285     * @param file        the file to write to, not null.
286     * @param charsetName the name of the requested charset, null uses the default Charset.
287     * @param append      true if content should be appended, false to overwrite.
288     * @throws NullPointerException if the file is null.
289     * @throws IOException          in case of an I/O error.
290     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
291     */
292    @Deprecated
293    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
294    public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
295        this(initWriter(file, charsetName, append));
296    }
297
298    private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
299        super(outputStreamWriter);
300    }
301
302    /**
303     * Constructs a FileWriterWithEncoding with a file encoding.
304     *
305     * @param fileName the name of the file to write to, not null.
306     * @param charset  the charset to use, not null.
307     * @throws NullPointerException if the file name or encoding is null.
308     * @throws IOException          in case of an I/O error.
309     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
310     */
311    @Deprecated
312    public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
313        this(new File(fileName), charset, false);
314    }
315
316    /**
317     * Constructs a FileWriterWithEncoding with a file encoding.
318     *
319     * @param fileName the name of the file to write to, not null.
320     * @param charset  the encoding to use, not null.
321     * @param append   true if content should be appended, false to overwrite.
322     * @throws NullPointerException if the file name or encoding is null.
323     * @throws IOException          in case of an I/O error.
324     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
325     */
326    @Deprecated
327    public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
328        this(new File(fileName), charset, append);
329    }
330
331    /**
332     * Constructs a FileWriterWithEncoding with a file encoding.
333     *
334     * @param fileName the name of the file to write to, not null.
335     * @param encoding the encoding to use, not null.
336     * @throws NullPointerException if the file name or encoding is null.
337     * @throws IOException          in case of an I/O error.
338     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
339     */
340    @Deprecated
341    public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
342        this(new File(fileName), encoding, false);
343    }
344
345    /**
346     * Constructs a FileWriterWithEncoding with a file encoding.
347     *
348     * @param fileName       the name of the file to write to, not null.
349     * @param charsetEncoder the encoding to use, not null.
350     * @param append         true if content should be appended, false to overwrite.
351     * @throws NullPointerException if the file name or encoding is null.
352     * @throws IOException          in case of an I/O error.
353     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
354     */
355    @Deprecated
356    public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
357        this(new File(fileName), charsetEncoder, append);
358    }
359
360    /**
361     * Constructs a FileWriterWithEncoding with a file encoding.
362     *
363     * @param fileName    the name of the file to write to, not null.
364     * @param charsetName the name of the requested charset, not null.
365     * @throws NullPointerException if the file name or encoding is null.
366     * @throws IOException          in case of an I/O error.
367     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
368     */
369    @Deprecated
370    public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
371        this(new File(fileName), charsetName, false);
372    }
373
374    /**
375     * Constructs a FileWriterWithEncoding with a file encoding.
376     *
377     * @param fileName    the name of the file to write to, not null.
378     * @param charsetName the name of the requested charset, not null.
379     * @param append      true if content should be appended, false to overwrite.
380     * @throws NullPointerException if the file name or encoding is null.
381     * @throws IOException          in case of an I/O error.
382     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
383     */
384    @Deprecated
385    public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
386        this(new File(fileName), charsetName, append);
387    }
388}