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.AbstractStreamBuilder;
032
033/**
034 * Writer of files that allows the encoding to be set.
035 * <p>
036 * This class provides a simple alternative to {@link FileWriter} that allows an encoding to be set. Unfortunately, it cannot subclass {@link FileWriter}.
037 * </p>
038 * <p>
039 * By default, the file will be overwritten, but this may be changed to append.
040 * </p>
041 * <p>
042 * 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
043 * required then use the {@link java.io.FileWriter} directly, rather than this implementation.
044 * </p>
045 *
046 * @since 1.4
047 */
048public class FileWriterWithEncoding extends ProxyWriter {
049
050    /**
051     * Builds a new {@link FileWriterWithEncoding} instance.
052     * <p>
053     * Using a CharsetEncoder:
054     * </p>
055     * <pre>{@code
056     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
057     *   .setPath(path)
058     *   .setAppend(false)
059     *   .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder())
060     *   .get()}
061     * </pre>
062     * <p>
063     * Using a Charset:
064     * </p>
065     * <pre>{@code
066     * FileWriterWithEncoding s = FileWriterWithEncoding.builder()
067     *   .setPath(path)
068     *   .setAppend(false)
069     *   .setCharsetEncoder(StandardCharsets.UTF_8)
070     *   .get()}
071     * </pre>
072     * @since 2.12.0
073     */
074    public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> {
075
076        private boolean append;
077
078        private CharsetEncoder charsetEncoder = super.getCharset().newEncoder();
079
080        /**
081         * Constructs a new instance.
082         *
083         * @throws UnsupportedOperationException if the origin cannot be converted to a File.
084         */
085        @SuppressWarnings("resource")
086        @Override
087        public FileWriterWithEncoding get() throws IOException {
088            if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) {
089                throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset()));
090            }
091            final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
092            return new FileWriterWithEncoding(FileWriterWithEncoding.initWriter(getOrigin().getFile(), encoder, append));
093        }
094
095        /**
096         * Sets whether or not to append.
097         *
098         * @param append Whether or not to append.
099         * @return this
100         */
101        public Builder setAppend(final boolean append) {
102            this.append = append;
103            return this;
104        }
105
106        /**
107         * Sets charsetEncoder to use for encoding.
108         *
109         * @param charsetEncoder The charsetEncoder to use for encoding.
110         * @return this
111         */
112        public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
113            this.charsetEncoder = charsetEncoder;
114            return this;
115        }
116
117    }
118
119    /**
120     * Constructs a new {@link Builder}.
121     *
122     * @return Creates a new {@link Builder}.
123     * @since 2.12.0
124     */
125    public static Builder builder() {
126        return new Builder();
127    }
128
129    /**
130     * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
131     *
132     * @param file     the file to be accessed
133     * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
134     * @param append   true to append
135     * @return a new initialized OutputStreamWriter
136     * @throws IOException if an error occurs
137     */
138    private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
139        Objects.requireNonNull(file, "file");
140        OutputStream outputStream = null;
141        final boolean fileExistedAlready = file.exists();
142        try {
143            outputStream = FileUtils.newOutputStream(file, append);
144            if (encoding == null || encoding instanceof Charset) {
145                return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
146            }
147            if (encoding instanceof CharsetEncoder) {
148                return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
149            }
150            return new OutputStreamWriter(outputStream, (String) encoding);
151        } catch (final IOException | RuntimeException ex) {
152            try {
153                IOUtils.close(outputStream);
154            } catch (final IOException e) {
155                ex.addSuppressed(e);
156            }
157            if (!fileExistedAlready) {
158                FileUtils.deleteQuietly(file);
159            }
160            throw ex;
161        }
162    }
163
164    /**
165     * Constructs a FileWriterWithEncoding with a file encoding.
166     *
167     * @param file    the file to write to, not null
168     * @param charset the encoding to use, not null
169     * @throws NullPointerException if the file or encoding is null
170     * @throws IOException          in case of an I/O error
171     * @deprecated Use {@link #builder()}
172     */
173    @Deprecated
174    public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
175        this(file, charset, false);
176    }
177
178    /**
179     * Constructs a FileWriterWithEncoding with a file encoding.
180     *
181     * @param file     the file to write to, not null.
182     * @param encoding the name of the requested charset, null uses the default Charset.
183     * @param append   true if content should be appended, false to overwrite.
184     * @throws NullPointerException if the file is null.
185     * @throws IOException          in case of an I/O error.
186     * @deprecated Use {@link #builder()}
187     */
188    @Deprecated
189    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
190    public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
191        this(initWriter(file, encoding, append));
192    }
193
194    /**
195     * Constructs a FileWriterWithEncoding with a file encoding.
196     *
197     * @param file           the file to write to, not null
198     * @param charsetEncoder the encoding to use, not null
199     * @throws NullPointerException if the file or encoding is null
200     * @throws IOException          in case of an I/O error
201     * @deprecated Use {@link #builder()}
202     */
203    @Deprecated
204    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
205        this(file, charsetEncoder, false);
206    }
207
208    /**
209     * Constructs a FileWriterWithEncoding with a file encoding.
210     *
211     * @param file           the file to write to, not null.
212     * @param charsetEncoder the encoding to use, null uses the default Charset.
213     * @param append         true if content should be appended, false to overwrite.
214     * @throws NullPointerException if the file is null.
215     * @throws IOException          in case of an I/O error.
216     * @deprecated Use {@link #builder()}
217     */
218    @Deprecated
219    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
220    public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
221        this(initWriter(file, charsetEncoder, append));
222    }
223
224    /**
225     * Constructs a FileWriterWithEncoding with a file encoding.
226     *
227     * @param file        the file to write to, not null
228     * @param charsetName the name of the requested charset, not null
229     * @throws NullPointerException if the file or encoding is null
230     * @throws IOException          in case of an I/O error
231     * @deprecated Use {@link #builder()}
232     */
233    @Deprecated
234    public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
235        this(file, charsetName, false);
236    }
237
238    /**
239     * Constructs a FileWriterWithEncoding with a file encoding.
240     *
241     * @param file        the file to write to, not null.
242     * @param charsetName the name of the requested charset, null uses the default Charset.
243     * @param append      true if content should be appended, false to overwrite.
244     * @throws NullPointerException if the file is null.
245     * @throws IOException          in case of an I/O error.
246     * @deprecated Use {@link #builder()}
247     */
248    @Deprecated
249    @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
250    public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
251        this(initWriter(file, charsetName, append));
252    }
253
254    private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
255        super(outputStreamWriter);
256    }
257
258    /**
259     * Constructs a FileWriterWithEncoding with a file encoding.
260     *
261     * @param fileName the name of the file to write to, not null
262     * @param charset  the charset to use, not null
263     * @throws NullPointerException if the file name or encoding is null
264     * @throws IOException          in case of an I/O error
265     * @deprecated Use {@link #builder()}
266     */
267    @Deprecated
268    public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
269        this(new File(fileName), charset, false);
270    }
271
272    /**
273     * Constructs a FileWriterWithEncoding with a file encoding.
274     *
275     * @param fileName the name of the file to write to, not null
276     * @param charset  the encoding to use, not null
277     * @param append   true if content should be appended, false to overwrite
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()}
281     */
282    @Deprecated
283    public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
284        this(new File(fileName), charset, append);
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 encoding the encoding to use, not null
292     * @throws NullPointerException if the file name or encoding is null
293     * @throws IOException          in case of an I/O error
294     * @deprecated Use {@link #builder()}
295     */
296    @Deprecated
297    public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
298        this(new File(fileName), encoding, false);
299    }
300
301    /**
302     * Constructs a FileWriterWithEncoding with a file encoding.
303     *
304     * @param fileName       the name of the file to write to, not null
305     * @param charsetEncoder the encoding to use, not null
306     * @param append         true if content should be appended, false to overwrite
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()}
310     */
311    @Deprecated
312    public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
313        this(new File(fileName), charsetEncoder, append);
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 charsetName the name of the requested charset, not null
321     * @throws NullPointerException if the file name or encoding is null
322     * @throws IOException          in case of an I/O error
323     * @deprecated Use {@link #builder()}
324     */
325    @Deprecated
326    public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
327        this(new File(fileName), charsetName, false);
328    }
329
330    /**
331     * Constructs a FileWriterWithEncoding with a file encoding.
332     *
333     * @param fileName    the name of the file to write to, not null
334     * @param charsetName the name of the requested charset, not null
335     * @param append      true if content should be appended, false to overwrite
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()}
339     */
340    @Deprecated
341    public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
342        this(new File(fileName), charsetName, append);
343    }
344}