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 java.io.FileWriter} directly, rather than this implementation.
045 * </p>
046 * <p>
047 * To build an instance, see {@link Builder}.
048 * </p>
049 *
050 * @since 1.4
051 */
052public class FileWriterWithEncoding extends ProxyWriter {
053
054    /**
055     * Builds a new {@link FileWriterWithEncoding} instance.
056     * <p>
057     * Using a CharsetEncoder:
058     * </p>
059     * <pre>{@code
060     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
061     *   .setPath(path)
062     *   .setAppend(false)
063     *   .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder())
064     *   .get();}
065     * </pre>
066     * <p>
067     * Using a Charset:
068     * </p>
069     * <pre>{@code
070     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
071     *   .setPath(path)
072     *   .setAppend(false)
073     *   .setCharsetEncoder(StandardCharsets.UTF_8)
074     *   .get();}
075     * </pre>
076     *
077     * @since 2.12.0
078     */
079    public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> {
080
081        private boolean append;
082
083        private CharsetEncoder charsetEncoder = super.getCharset().newEncoder();
084
085        /**
086         * Constructs a new instance.
087         * <p>
088         * This builder use the aspects File, CharsetEncoder, and append.
089         * </p>
090         * <p>
091         * You must provide an origin that can be converted to a File by this builder, otherwise, this call will throw an
092         * {@link UnsupportedOperationException}.
093         * </p>
094         *
095         * @return a new instance.
096         * @throws UnsupportedOperationException if the origin cannot provide a File.
097         * @throws IllegalStateException if the {@code origin} is {@code null}.
098         * @see AbstractOrigin#getFile()
099         */
100        @SuppressWarnings("resource")
101        @Override
102        public FileWriterWithEncoding get() throws IOException {
103            if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) {
104                throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset()));
105            }
106            final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
107            return new FileWriterWithEncoding(FileWriterWithEncoding.initWriter(checkOrigin().getFile(), encoder, append));
108        }
109
110        /**
111         * Sets whether or not to append.
112         *
113         * @param append Whether or not to append.
114         * @return this
115         */
116        public Builder setAppend(final boolean append) {
117            this.append = append;
118            return this;
119        }
120
121        /**
122         * Sets charsetEncoder to use for encoding.
123         *
124         * @param charsetEncoder The charsetEncoder to use for encoding.
125         * @return this
126         */
127        public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
128            this.charsetEncoder = charsetEncoder;
129            return this;
130        }
131
132    }
133
134    /**
135     * Constructs a new {@link Builder}.
136     *
137     * @return Creates a new {@link Builder}.
138     * @since 2.12.0
139     */
140    public static Builder builder() {
141        return new Builder();
142    }
143
144    /**
145     * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
146     *
147     * @param file     the file to be accessed
148     * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
149     * @param append   true to append
150     * @return a new initialized OutputStreamWriter
151     * @throws IOException if an error occurs
152     */
153    private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
154        Objects.requireNonNull(file, "file");
155        OutputStream outputStream = null;
156        final boolean fileExistedAlready = file.exists();
157        try {
158            outputStream = FileUtils.newOutputStream(file, append);
159            if (encoding == null || encoding instanceof Charset) {
160                return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
161            }
162            if (encoding instanceof CharsetEncoder) {
163                return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
164            }
165            return new OutputStreamWriter(outputStream, (String) encoding);
166        } catch (final IOException | RuntimeException ex) {
167            try {
168                IOUtils.close(outputStream);
169            } catch (final IOException e) {
170                ex.addSuppressed(e);
171            }
172            if (!fileExistedAlready) {
173                FileUtils.deleteQuietly(file);
174            }
175            throw ex;
176        }
177    }
178
179    /**
180     * Constructs a FileWriterWithEncoding with a file encoding.
181     *
182     * @param file    the file to write to, not null
183     * @param charset the encoding to use, not null
184     * @throws NullPointerException if the file or encoding is null
185     * @throws IOException          in case of an I/O error
186     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
187     */
188    @Deprecated
189    public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
190        this(file, charset, false);
191    }
192
193    /**
194     * Constructs a FileWriterWithEncoding with a file encoding.
195     *
196     * @param file     the file to write to, not null.
197     * @param encoding the name of the requested charset, null uses the default Charset.
198     * @param append   true if content should be appended, false to overwrite.
199     * @throws NullPointerException if the file is null.
200     * @throws IOException          in case of an I/O error.
201     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
202     */
203    @Deprecated
204    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
205    public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
206        this(initWriter(file, encoding, 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 charsetEncoder 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 CharsetEncoder charsetEncoder) throws IOException {
220        this(file, charsetEncoder, 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 charsetEncoder the encoding to use, 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 CharsetEncoder charsetEncoder, final boolean append) throws IOException {
236        this(initWriter(file, charsetEncoder, 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 charsetName the name of the requested charset, 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 String charsetName) throws IOException {
250        this(file, charsetName, 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 charsetName the name of the requested charset, 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 String charsetName, final boolean append) throws IOException {
266        this(initWriter(file, charsetName, append));
267    }
268
269    private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
270        super(outputStreamWriter);
271    }
272
273    /**
274     * Constructs a FileWriterWithEncoding with a file encoding.
275     *
276     * @param fileName the name of the file to write to, not null
277     * @param charset  the charset to use, not null
278     * @throws NullPointerException if the file name or encoding is null
279     * @throws IOException          in case of an I/O error
280     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
281     */
282    @Deprecated
283    public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
284        this(new File(fileName), charset, false);
285    }
286
287    /**
288     * Constructs a FileWriterWithEncoding with a file encoding.
289     *
290     * @param fileName the name of the file to write to, not null
291     * @param charset  the encoding to use, not null
292     * @param append   true if content should be appended, false to overwrite
293     * @throws NullPointerException if the file name or encoding is null
294     * @throws IOException          in case of an I/O error
295     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
296     */
297    @Deprecated
298    public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
299        this(new File(fileName), charset, append);
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 encoding the encoding 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 CharsetEncoder encoding) throws IOException {
313        this(new File(fileName), encoding, 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 charsetEncoder 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 CharsetEncoder charsetEncoder, final boolean append) throws IOException {
328        this(new File(fileName), charsetEncoder, 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 charsetName the name of the requested charset, 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 String charsetName) throws IOException {
342        this(new File(fileName), charsetName, 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 charsetName the name of the requested charset, 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 String charsetName, final boolean append) throws IOException {
357        this(new File(fileName), charsetName, append);
358    }
359}