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             try {
192                 IOUtils.close(outputStream);
193             } catch (final IOException e) {
194                 ex.addSuppressed(e);
195             }
196             if (!fileExistedAlready) {
197                 FileUtils.deleteQuietly(file);
198             }
199             throw ex;
200         }
201     }
202 
203     @SuppressWarnings("resource") // caller closes
204     private FileWriterWithEncoding(final Builder builder) throws IOException {
205         super(initWriter(builder.checkOriginFile(), builder.getEncoder(), builder.append));
206     }
207 
208     /**
209      * Constructs a FileWriterWithEncoding with a file encoding.
210      *
211      * @param file    the file to write to, not null.
212      * @param charset the encoding to use, not null.
213      * @throws NullPointerException if the file or encoding is null.
214      * @throws IOException          in case of an I/O error.
215      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
216      */
217     @Deprecated
218     public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
219         this(file, charset, false);
220     }
221 
222     /**
223      * Constructs a FileWriterWithEncoding with a file encoding.
224      *
225      * @param file     the file to write to, not null.
226      * @param encoding the name of the requested charset, null uses the default Charset.
227      * @param append   true if content should be appended, false to overwrite.
228      * @throws NullPointerException if the file is null.
229      * @throws IOException          in case of an I/O error.
230      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
231      */
232     @Deprecated
233     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
234     public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
235         this(initWriter(file, encoding, append));
236     }
237 
238     /**
239      * Constructs a FileWriterWithEncoding with a file encoding.
240      *
241      * @param file           the file to write to, not null.
242      * @param charsetEncoder the encoding to use, not null.
243      * @throws NullPointerException if the file or encoding is null.
244      * @throws IOException          in case of an I/O error.
245      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
246      */
247     @Deprecated
248     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
249         this(file, charsetEncoder, false);
250     }
251 
252     /**
253      * Constructs a FileWriterWithEncoding with a file encoding.
254      *
255      * @param file           the file to write to, not null.
256      * @param charsetEncoder the encoding to use, null uses the default Charset.
257      * @param append         true if content should be appended, false to overwrite.
258      * @throws NullPointerException if the file is null.
259      * @throws IOException          in case of an I/O error.
260      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
261      */
262     @Deprecated
263     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
264     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
265         this(initWriter(file, charsetEncoder, append));
266     }
267 
268     /**
269      * Constructs a FileWriterWithEncoding with a file encoding.
270      *
271      * @param file        the file to write to, not null.
272      * @param charsetName the name of the requested charset, not null.
273      * @throws NullPointerException if the file or encoding is null.
274      * @throws IOException          in case of an I/O error.
275      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
276      */
277     @Deprecated
278     public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
279         this(file, charsetName, false);
280     }
281 
282     /**
283      * Constructs a FileWriterWithEncoding with a file encoding.
284      *
285      * @param file        the file to write to, not null.
286      * @param charsetName the name of the requested charset, null uses the default Charset.
287      * @param append      true if content should be appended, false to overwrite.
288      * @throws NullPointerException if the file is null.
289      * @throws IOException          in case of an I/O error.
290      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
291      */
292     @Deprecated
293     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
294     public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
295         this(initWriter(file, charsetName, append));
296     }
297 
298     private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
299         super(outputStreamWriter);
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 charset  the charset 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 Charset charset) throws IOException {
313         this(new File(fileName), charset, 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 charset  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 Charset charset, final boolean append) throws IOException {
328         this(new File(fileName), charset, 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 encoding the encoding to use, 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 CharsetEncoder encoding) throws IOException {
342         this(new File(fileName), encoding, 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 charsetEncoder the encoding to use, 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 CharsetEncoder charsetEncoder, final boolean append) throws IOException {
357         this(new File(fileName), charsetEncoder, append);
358     }
359 
360     /**
361      * Constructs a FileWriterWithEncoding with a file encoding.
362      *
363      * @param fileName    the name of the file to write to, not null.
364      * @param charsetName the name of the requested charset, not null.
365      * @throws NullPointerException if the file name or encoding is null.
366      * @throws IOException          in case of an I/O error.
367      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
368      */
369     @Deprecated
370     public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
371         this(new File(fileName), charsetName, false);
372     }
373 
374     /**
375      * Constructs a FileWriterWithEncoding with a file encoding.
376      *
377      * @param fileName    the name of the file to write to, not null.
378      * @param charsetName the name of the requested charset, not null.
379      * @param append      true if content should be appended, false to overwrite.
380      * @throws NullPointerException if the file name or encoding is null.
381      * @throws IOException          in case of an I/O error.
382      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
383      */
384     @Deprecated
385     public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
386         this(new File(fileName), charsetName, append);
387     }
388 }