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