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 }