View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.output;
18  
19  import java.io.File;
20  import java.io.FileWriter;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.OutputStreamWriter;
24  import java.nio.charset.Charset;
25  import java.nio.charset.CharsetEncoder;
26  import java.util.Objects;
27  
28  import org.apache.commons.io.Charsets;
29  import org.apache.commons.io.FileUtils;
30  import org.apache.commons.io.IOUtils;
31  import org.apache.commons.io.build.AbstractOrigin;
32  import org.apache.commons.io.build.AbstractStreamBuilder;
33  
34  /**
35   * Writer of files that allows the encoding to be set.
36   * <p>
37   * This class provides a simple alternative to {@link FileWriter} that allows an encoding to be set. Unfortunately, it cannot subclass {@link FileWriter}.
38   * </p>
39   * <p>
40   * By default, the file will be overwritten, but this may be changed to append.
41   * </p>
42   * <p>
43   * 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
44   * required then use the {@link FileWriter} directly, rather than this implementation.
45   * </p>
46   * <p>
47   * To build an instance, use {@link Builder}.
48   * </p>
49   *
50   * @see Builder
51   * @since 1.4
52   */
53  public class FileWriterWithEncoding extends ProxyWriter {
54  
55      // @formatter:off
56      /**
57       * Builds a new {@link FileWriterWithEncoding}.
58       *
59       * <p>
60       * Using a CharsetEncoder:
61       * </p>
62       * <pre>{@code
63       * FileWriterWithEncoding w = FileWriterWithEncoding.builder()
64       *   .setPath(path)
65       *   .setAppend(false)
66       *   .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder())
67       *   .get();}
68       * </pre>
69       * <p>
70       * Using a Charset:
71       * </p>
72       * <pre>{@code
73       * FileWriterWithEncoding w = FileWriterWithEncoding.builder()
74       *   .setPath(path)
75       *   .setAppend(false)
76       *   .setCharsetEncoder(StandardCharsets.UTF_8)
77       *   .get();}
78       * </pre>
79       *
80       * @see #get()
81       * @since 2.12.0
82       */
83      // @formatter:on
84      public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> {
85  
86          private boolean append;
87  
88          private CharsetEncoder charsetEncoder = super.getCharset().newEncoder();
89  
90          /**
91           * Constructs a new builder of {@link FileWriterWithEncoding}.
92           */
93          public Builder() {
94              // empty
95          }
96  
97          /**
98           * Builds a new {@link FileWriterWithEncoding}.
99           * <p>
100          * You must set an aspect that supports {@link File} on this builder, otherwise, this method throws an exception.
101          * </p>
102          * <p>
103          * This builder uses the following aspects:
104          * </p>
105          * <ul>
106          * <li>{@link File} is the target aspect.</li>
107          * <li>{@link CharsetEncoder}</li>
108          * <li>append</li>
109          * </ul>
110          *
111          * @return a new instance.
112          * @throws UnsupportedOperationException if the origin cannot provide a File.
113          * @throws IllegalStateException         if the {@code origin} is {@code null}.
114          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link #getFile()}.
115          * @see AbstractOrigin#getFile()
116          * @see #getUnchecked()
117          */
118         @Override
119         public FileWriterWithEncoding get() throws IOException {
120             if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) {
121                 throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset()));
122             }
123             final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
124             return new FileWriterWithEncoding(initWriter(checkOrigin().getFile(), encoder, append));
125         }
126 
127         /**
128          * Sets whether or not to append.
129          *
130          * @param append Whether or not to append.
131          * @return {@code this} instance.
132          */
133         public Builder setAppend(final boolean append) {
134             this.append = append;
135             return this;
136         }
137 
138         /**
139          * Sets charsetEncoder to use for encoding.
140          *
141          * @param charsetEncoder The charsetEncoder to use for encoding.
142          * @return {@code this} instance.
143          */
144         public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
145             this.charsetEncoder = charsetEncoder;
146             return this;
147         }
148 
149     }
150 
151     /**
152      * Constructs a new {@link Builder}.
153      *
154      * @return Creates a new {@link Builder}.
155      * @since 2.12.0
156      */
157     public static Builder builder() {
158         return new Builder();
159     }
160 
161     /**
162      * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
163      *
164      * @param file     the file to be accessed.
165      * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
166      * @param append   true to append.
167      * @return a new initialized OutputStreamWriter.
168      * @throws IOException if an I/O error occurs.
169      */
170     private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
171         Objects.requireNonNull(file, "file");
172         OutputStream outputStream = null;
173         final boolean fileExistedAlready = file.exists();
174         try {
175             outputStream = FileUtils.newOutputStream(file, append);
176             if (encoding == null || encoding instanceof Charset) {
177                 return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
178             }
179             if (encoding instanceof CharsetEncoder) {
180                 return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
181             }
182             return new OutputStreamWriter(outputStream, (String) encoding);
183         } catch (final IOException | RuntimeException ex) {
184             try {
185                 IOUtils.close(outputStream);
186             } catch (final IOException e) {
187                 ex.addSuppressed(e);
188             }
189             if (!fileExistedAlready) {
190                 FileUtils.deleteQuietly(file);
191             }
192             throw ex;
193         }
194     }
195 
196     /**
197      * Constructs a FileWriterWithEncoding with a file encoding.
198      *
199      * @param file    the file to write to, not null
200      * @param charset the encoding to use, not null
201      * @throws NullPointerException if the file or encoding is null
202      * @throws IOException          in case of an I/O error
203      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
204      */
205     @Deprecated
206     public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
207         this(file, charset, false);
208     }
209 
210     /**
211      * Constructs a FileWriterWithEncoding with a file encoding.
212      *
213      * @param file     the file to write to, not null.
214      * @param encoding the name of the requested charset, null uses the default Charset.
215      * @param append   true if content should be appended, false to overwrite.
216      * @throws NullPointerException if the file is null.
217      * @throws IOException          in case of an I/O error.
218      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
219      */
220     @Deprecated
221     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
222     public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
223         this(initWriter(file, encoding, append));
224     }
225 
226     /**
227      * Constructs a FileWriterWithEncoding with a file encoding.
228      *
229      * @param file           the file to write to, not null
230      * @param charsetEncoder the encoding to use, not null
231      * @throws NullPointerException if the file or encoding is null
232      * @throws IOException          in case of an I/O error
233      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
234      */
235     @Deprecated
236     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
237         this(file, charsetEncoder, false);
238     }
239 
240     /**
241      * Constructs a FileWriterWithEncoding with a file encoding.
242      *
243      * @param file           the file to write to, not null.
244      * @param charsetEncoder the encoding to use, null uses the default Charset.
245      * @param append         true if content should be appended, false to overwrite.
246      * @throws NullPointerException if the file is null.
247      * @throws IOException          in case of an I/O error.
248      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
249      */
250     @Deprecated
251     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
252     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
253         this(initWriter(file, charsetEncoder, append));
254     }
255 
256     /**
257      * Constructs a FileWriterWithEncoding with a file encoding.
258      *
259      * @param file        the file to write to, not null
260      * @param charsetName the name of the requested charset, not null
261      * @throws NullPointerException if the file or encoding is null
262      * @throws IOException          in case of an I/O error
263      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
264      */
265     @Deprecated
266     public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
267         this(file, charsetName, false);
268     }
269 
270     /**
271      * Constructs a FileWriterWithEncoding with a file encoding.
272      *
273      * @param file        the file to write to, not null.
274      * @param charsetName the name of the requested charset, null uses the default Charset.
275      * @param append      true if content should be appended, false to overwrite.
276      * @throws NullPointerException if the file is null.
277      * @throws IOException          in case of an I/O error.
278      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
279      */
280     @Deprecated
281     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
282     public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
283         this(initWriter(file, charsetName, append));
284     }
285 
286     private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
287         super(outputStreamWriter);
288     }
289 
290     /**
291      * Constructs a FileWriterWithEncoding with a file encoding.
292      *
293      * @param fileName the name of the file to write to, not null
294      * @param charset  the charset to use, not null
295      * @throws NullPointerException if the file name or encoding is null
296      * @throws IOException          in case of an I/O error
297      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
298      */
299     @Deprecated
300     public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
301         this(new File(fileName), charset, false);
302     }
303 
304     /**
305      * Constructs a FileWriterWithEncoding with a file encoding.
306      *
307      * @param fileName the name of the file to write to, not null
308      * @param charset  the encoding to use, not null
309      * @param append   true if content should be appended, false to overwrite
310      * @throws NullPointerException if the file name or encoding is null
311      * @throws IOException          in case of an I/O error
312      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
313      */
314     @Deprecated
315     public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
316         this(new File(fileName), charset, append);
317     }
318 
319     /**
320      * Constructs a FileWriterWithEncoding with a file encoding.
321      *
322      * @param fileName the name of the file to write to, not null
323      * @param encoding the encoding to use, not null
324      * @throws NullPointerException if the file name or encoding is null
325      * @throws IOException          in case of an I/O error
326      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
327      */
328     @Deprecated
329     public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
330         this(new File(fileName), encoding, false);
331     }
332 
333     /**
334      * Constructs a FileWriterWithEncoding with a file encoding.
335      *
336      * @param fileName       the name of the file to write to, not null
337      * @param charsetEncoder the encoding to use, not null
338      * @param append         true if content should be appended, false to overwrite
339      * @throws NullPointerException if the file name or encoding is null
340      * @throws IOException          in case of an I/O error
341      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
342      */
343     @Deprecated
344     public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
345         this(new File(fileName), charsetEncoder, append);
346     }
347 
348     /**
349      * Constructs a FileWriterWithEncoding with a file encoding.
350      *
351      * @param fileName    the name of the file to write to, not null
352      * @param charsetName the name of the requested charset, not null
353      * @throws NullPointerException if the file name or encoding is null
354      * @throws IOException          in case of an I/O error
355      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
356      */
357     @Deprecated
358     public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
359         this(new File(fileName), charsetName, false);
360     }
361 
362     /**
363      * Constructs a FileWriterWithEncoding with a file encoding.
364      *
365      * @param fileName    the name of the file to write to, not null
366      * @param charsetName the name of the requested charset, not null
367      * @param append      true if content should be appended, false to overwrite
368      * @throws NullPointerException if the file name or encoding is null
369      * @throws IOException          in case of an I/O error
370      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
371      */
372     @Deprecated
373     public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
374         this(new File(fileName), charsetName, append);
375     }
376 }