001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.io.output; 018 019import java.io.File; 020import java.io.FileWriter; 021import java.io.IOException; 022import java.io.OutputStream; 023import java.io.OutputStreamWriter; 024import java.nio.charset.Charset; 025import java.nio.charset.CharsetEncoder; 026import java.util.Objects; 027 028import org.apache.commons.io.Charsets; 029import org.apache.commons.io.FileUtils; 030import org.apache.commons.io.IOUtils; 031import org.apache.commons.io.build.AbstractOrigin; 032import org.apache.commons.io.build.AbstractStreamBuilder; 033 034/** 035 * Writer of files that allows the encoding to be set. 036 * <p> 037 * This class provides a simple alternative to {@link FileWriter} that allows an encoding to be set. Unfortunately, it cannot subclass {@link FileWriter}. 038 * </p> 039 * <p> 040 * By default, the file will be overwritten, but this may be changed to append. 041 * </p> 042 * <p> 043 * 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 044 * required then use the {@link FileWriter} directly, rather than this implementation. 045 * </p> 046 * <p> 047 * To build an instance, use {@link Builder}. 048 * </p> 049 * 050 * @see Builder 051 * @since 1.4 052 */ 053public class FileWriterWithEncoding extends ProxyWriter { 054 055 // @formatter:off 056 /** 057 * Builds a new {@link FileWriterWithEncoding}. 058 * 059 * <p> 060 * Using a CharsetEncoder: 061 * </p> 062 * <pre>{@code 063 * FileWriterWithEncoding w = FileWriterWithEncoding.builder() 064 * .setPath(path) 065 * .setAppend(false) 066 * .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder()) 067 * .get();} 068 * </pre> 069 * <p> 070 * Using a Charset: 071 * </p> 072 * <pre>{@code 073 * FileWriterWithEncoding w = FileWriterWithEncoding.builder() 074 * .setPath(path) 075 * .setAppend(false) 076 * .setCharsetEncoder(StandardCharsets.UTF_8) 077 * .get();} 078 * </pre> 079 * 080 * @see #get() 081 * @since 2.12.0 082 */ 083 // @formatter:on 084 public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> { 085 086 private boolean append; 087 088 private CharsetEncoder charsetEncoder = super.getCharset().newEncoder(); 089 090 /** 091 * Constructs a new builder of {@link FileWriterWithEncoding}. 092 */ 093 public Builder() { 094 // empty 095 } 096 097 private File checkOriginFile() { 098 return checkOrigin().getFile(); 099 } 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}