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    *      https://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          private File checkOriginFile() {
98              return checkOrigin().getFile();
99          }
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             final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
132             return encoder;
133         }
134 
135         /**
136          * Sets whether or not to append.
137          *
138          * @param append Whether or not to append.
139          * @return {@code this} instance.
140          */
141         public Builder setAppend(final boolean append) {
142             this.append = append;
143             return this;
144         }
145 
146         /**
147          * Sets charsetEncoder to use for encoding.
148          *
149          * @param charsetEncoder The charsetEncoder to use for encoding.
150          * @return {@code this} instance.
151          */
152         public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
153             this.charsetEncoder = charsetEncoder;
154             return this;
155         }
156 
157     }
158 
159     /**
160      * Constructs a new {@link Builder}.
161      *
162      * @return Creates a new {@link Builder}.
163      * @since 2.12.0
164      */
165     public static Builder builder() {
166         return new Builder();
167     }
168 
169     /**
170      * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
171      *
172      * @param file     the file to be accessed.
173      * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
174      * @param append   true to append.
175      * @return a new initialized OutputStreamWriter.
176      * @throws IOException if an I/O error occurs.
177      */
178     private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
179         Objects.requireNonNull(file, "file");
180         OutputStream outputStream = null;
181         final boolean fileExistedAlready = file.exists();
182         try {
183             outputStream = FileUtils.newOutputStream(file, append);
184             if (encoding == null || encoding instanceof Charset) {
185                 return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
186             }
187             if (encoding instanceof CharsetEncoder) {
188                 return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
189             }
190             return new OutputStreamWriter(outputStream, (String) encoding);
191         } catch (final IOException | RuntimeException ex) {
192             try {
193                 IOUtils.close(outputStream);
194             } catch (final IOException e) {
195                 ex.addSuppressed(e);
196             }
197             if (!fileExistedAlready) {
198                 FileUtils.deleteQuietly(file);
199             }
200             throw ex;
201         }
202     }
203 
204     @SuppressWarnings("resource") // caller closes
205     private FileWriterWithEncoding(final Builder builder) throws IOException {
206         super(initWriter(builder.checkOriginFile(), builder.getEncoder(), builder.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 charset 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 Charset charset) throws IOException {
220         this(file, charset, 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 encoding the name of the requested charset, 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 Charset encoding, final boolean append) throws IOException {
236         this(initWriter(file, encoding, 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 charsetEncoder the encoding to use, 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 CharsetEncoder charsetEncoder) throws IOException {
250         this(file, charsetEncoder, 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 charsetEncoder the encoding to use, 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 CharsetEncoder charsetEncoder, final boolean append) throws IOException {
266         this(initWriter(file, charsetEncoder, append));
267     }
268 
269     /**
270      * Constructs a FileWriterWithEncoding with a file encoding.
271      *
272      * @param file        the file to write to, not null
273      * @param charsetName the name of the requested charset, not null
274      * @throws NullPointerException if the file or encoding is null
275      * @throws IOException          in case of an I/O error
276      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
277      */
278     @Deprecated
279     public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
280         this(file, charsetName, false);
281     }
282 
283     /**
284      * Constructs a FileWriterWithEncoding with a file encoding.
285      *
286      * @param file        the file to write to, not null.
287      * @param charsetName the name of the requested charset, null uses the default Charset.
288      * @param append      true if content should be appended, false to overwrite.
289      * @throws NullPointerException if the file is null.
290      * @throws IOException          in case of an I/O error.
291      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
292      */
293     @Deprecated
294     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
295     public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
296         this(initWriter(file, charsetName, append));
297     }
298 
299     private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
300         super(outputStreamWriter);
301     }
302 
303     /**
304      * Constructs a FileWriterWithEncoding with a file encoding.
305      *
306      * @param fileName the name of the file to write to, not null
307      * @param charset  the charset to use, not null
308      * @throws NullPointerException if the file name or encoding is null
309      * @throws IOException          in case of an I/O error
310      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
311      */
312     @Deprecated
313     public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
314         this(new File(fileName), charset, false);
315     }
316 
317     /**
318      * Constructs a FileWriterWithEncoding with a file encoding.
319      *
320      * @param fileName the name of the file to write to, not null
321      * @param charset  the encoding to use, not null
322      * @param append   true if content should be appended, false to overwrite
323      * @throws NullPointerException if the file name or encoding is null
324      * @throws IOException          in case of an I/O error
325      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
326      */
327     @Deprecated
328     public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
329         this(new File(fileName), charset, append);
330     }
331 
332     /**
333      * Constructs a FileWriterWithEncoding with a file encoding.
334      *
335      * @param fileName the name of the file to write to, not null
336      * @param encoding the encoding to use, not null
337      * @throws NullPointerException if the file name or encoding is null
338      * @throws IOException          in case of an I/O error
339      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
340      */
341     @Deprecated
342     public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
343         this(new File(fileName), encoding, false);
344     }
345 
346     /**
347      * Constructs a FileWriterWithEncoding with a file encoding.
348      *
349      * @param fileName       the name of the file to write to, not null
350      * @param charsetEncoder the encoding to use, not null
351      * @param append         true if content should be appended, false to overwrite
352      * @throws NullPointerException if the file name or encoding is null
353      * @throws IOException          in case of an I/O error
354      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
355      */
356     @Deprecated
357     public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
358         this(new File(fileName), charsetEncoder, append);
359     }
360 
361     /**
362      * Constructs a FileWriterWithEncoding with a file encoding.
363      *
364      * @param fileName    the name of the file to write to, not null
365      * @param charsetName the name of the requested charset, not null
366      * @throws NullPointerException if the file name or encoding is null
367      * @throws IOException          in case of an I/O error
368      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
369      */
370     @Deprecated
371     public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
372         this(new File(fileName), charsetName, false);
373     }
374 
375     /**
376      * Constructs a FileWriterWithEncoding with a file encoding.
377      *
378      * @param fileName    the name of the file to write to, not null
379      * @param charsetName the name of the requested charset, not null
380      * @param append      true if content should be appended, false to overwrite
381      * @throws NullPointerException if the file name or encoding is null
382      * @throws IOException          in case of an I/O error
383      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
384      */
385     @Deprecated
386     public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
387         this(new File(fileName), charsetName, append);
388     }
389 }