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 }