FileWriterWithEncoding.java

  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. import java.io.File;
  19. import java.io.FileWriter;
  20. import java.io.IOException;
  21. import java.io.OutputStream;
  22. import java.io.OutputStreamWriter;
  23. import java.nio.charset.Charset;
  24. import java.nio.charset.CharsetEncoder;
  25. import java.util.Objects;

  26. import org.apache.commons.io.Charsets;
  27. import org.apache.commons.io.FileUtils;
  28. import org.apache.commons.io.IOUtils;
  29. import org.apache.commons.io.build.AbstractOrigin;
  30. import org.apache.commons.io.build.AbstractStreamBuilder;

  31. /**
  32.  * Writer of files that allows the encoding to be set.
  33.  * <p>
  34.  * This class provides a simple alternative to {@link FileWriter} that allows an encoding to be set. Unfortunately, it cannot subclass {@link FileWriter}.
  35.  * </p>
  36.  * <p>
  37.  * By default, the file will be overwritten, but this may be changed to append.
  38.  * </p>
  39.  * <p>
  40.  * 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
  41.  * required then use the {@link FileWriter} directly, rather than this implementation.
  42.  * </p>
  43.  * <p>
  44.  * To build an instance, use {@link Builder}.
  45.  * </p>
  46.  *
  47.  * @see Builder
  48.  * @since 1.4
  49.  */
  50. public class FileWriterWithEncoding extends ProxyWriter {

  51.     // @formatter:off
  52.     /**
  53.      * Builds a new {@link FileWriterWithEncoding}.
  54.      *
  55.      * <p>
  56.      * Using a CharsetEncoder:
  57.      * </p>
  58.      * <pre>{@code
  59.      * FileWriterWithEncoding w = FileWriterWithEncoding.builder()
  60.      *   .setPath(path)
  61.      *   .setAppend(false)
  62.      *   .setCharsetEncoder(StandardCharsets.UTF_8.newEncoder())
  63.      *   .get();}
  64.      * </pre>
  65.      * <p>
  66.      * Using a Charset:
  67.      * </p>
  68.      * <pre>{@code
  69.      * FileWriterWithEncoding w = FileWriterWithEncoding.builder()
  70.      *   .setPath(path)
  71.      *   .setAppend(false)
  72.      *   .setCharsetEncoder(StandardCharsets.UTF_8)
  73.      *   .get();}
  74.      * </pre>
  75.      *
  76.      * @see #get()
  77.      * @since 2.12.0
  78.      */
  79.     // @formatter:on
  80.     public static class Builder extends AbstractStreamBuilder<FileWriterWithEncoding, Builder> {

  81.         private boolean append;

  82.         private CharsetEncoder charsetEncoder = super.getCharset().newEncoder();

  83.         /**
  84.          * Constructs a new builder of {@link FileWriterWithEncoding}.
  85.          */
  86.         public Builder() {
  87.             // empty
  88.         }

  89.         private File checkOriginFile() {
  90.             return checkOrigin().getFile();
  91.         }

  92.         /**
  93.          * Builds a new {@link FileWriterWithEncoding}.
  94.          * <p>
  95.          * You must set an aspect that supports {@link File} on this builder, otherwise, this method throws an exception.
  96.          * </p>
  97.          * <p>
  98.          * This builder uses the following aspects:
  99.          * </p>
  100.          * <ul>
  101.          * <li>{@link File} is the target aspect.</li>
  102.          * <li>{@link CharsetEncoder}</li>
  103.          * <li>append</li>
  104.          * </ul>
  105.          *
  106.          * @return a new instance.
  107.          * @throws UnsupportedOperationException if the origin cannot provide a File.
  108.          * @throws IllegalStateException         if the {@code origin} is {@code null}.
  109.          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link #getFile()}.
  110.          * @see AbstractOrigin#getFile()
  111.          * @see #getUnchecked()
  112.          */
  113.         @Override
  114.         public FileWriterWithEncoding get() throws IOException {
  115.             return new FileWriterWithEncoding(this);
  116.         }

  117.         private Object getEncoder() {
  118.             if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) {
  119.                 throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset()));
  120.             }
  121.             final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset();
  122.             return encoder;
  123.         }

  124.         /**
  125.          * Sets whether or not to append.
  126.          *
  127.          * @param append Whether or not to append.
  128.          * @return {@code this} instance.
  129.          */
  130.         public Builder setAppend(final boolean append) {
  131.             this.append = append;
  132.             return this;
  133.         }

  134.         /**
  135.          * Sets charsetEncoder to use for encoding.
  136.          *
  137.          * @param charsetEncoder The charsetEncoder to use for encoding.
  138.          * @return {@code this} instance.
  139.          */
  140.         public Builder setCharsetEncoder(final CharsetEncoder charsetEncoder) {
  141.             this.charsetEncoder = charsetEncoder;
  142.             return this;
  143.         }

  144.     }

  145.     /**
  146.      * Constructs a new {@link Builder}.
  147.      *
  148.      * @return Creates a new {@link Builder}.
  149.      * @since 2.12.0
  150.      */
  151.     public static Builder builder() {
  152.         return new Builder();
  153.     }

  154.     /**
  155.      * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
  156.      *
  157.      * @param file     the file to be accessed.
  158.      * @param encoding the encoding to use - may be Charset, CharsetEncoder or String, null uses the default Charset.
  159.      * @param append   true to append.
  160.      * @return a new initialized OutputStreamWriter.
  161.      * @throws IOException if an I/O error occurs.
  162.      */
  163.     private static OutputStreamWriter initWriter(final File file, final Object encoding, final boolean append) throws IOException {
  164.         Objects.requireNonNull(file, "file");
  165.         OutputStream outputStream = null;
  166.         final boolean fileExistedAlready = file.exists();
  167.         try {
  168.             outputStream = FileUtils.newOutputStream(file, append);
  169.             if (encoding == null || encoding instanceof Charset) {
  170.                 return new OutputStreamWriter(outputStream, Charsets.toCharset((Charset) encoding));
  171.             }
  172.             if (encoding instanceof CharsetEncoder) {
  173.                 return new OutputStreamWriter(outputStream, (CharsetEncoder) encoding);
  174.             }
  175.             return new OutputStreamWriter(outputStream, (String) encoding);
  176.         } catch (final IOException | RuntimeException ex) {
  177.             try {
  178.                 IOUtils.close(outputStream);
  179.             } catch (final IOException e) {
  180.                 ex.addSuppressed(e);
  181.             }
  182.             if (!fileExistedAlready) {
  183.                 FileUtils.deleteQuietly(file);
  184.             }
  185.             throw ex;
  186.         }
  187.     }

  188.     @SuppressWarnings("resource") // caller closes
  189.     private FileWriterWithEncoding(final Builder builder) throws IOException {
  190.         super(initWriter(builder.checkOriginFile(), builder.getEncoder(), builder.append));
  191.     }

  192.     /**
  193.      * Constructs a FileWriterWithEncoding with a file encoding.
  194.      *
  195.      * @param file    the file to write to, not null
  196.      * @param charset the encoding to use, not null
  197.      * @throws NullPointerException if the file or encoding is null
  198.      * @throws IOException          in case of an I/O error
  199.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  200.      */
  201.     @Deprecated
  202.     public FileWriterWithEncoding(final File file, final Charset charset) throws IOException {
  203.         this(file, charset, false);
  204.     }

  205.     /**
  206.      * Constructs a FileWriterWithEncoding with a file encoding.
  207.      *
  208.      * @param file     the file to write to, not null.
  209.      * @param encoding the name of the requested charset, null uses the default Charset.
  210.      * @param append   true if content should be appended, false to overwrite.
  211.      * @throws NullPointerException if the file is null.
  212.      * @throws IOException          in case of an I/O error.
  213.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  214.      */
  215.     @Deprecated
  216.     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
  217.     public FileWriterWithEncoding(final File file, final Charset encoding, final boolean append) throws IOException {
  218.         this(initWriter(file, encoding, append));
  219.     }

  220.     /**
  221.      * Constructs a FileWriterWithEncoding with a file encoding.
  222.      *
  223.      * @param file           the file to write to, not null
  224.      * @param charsetEncoder the encoding to use, not null
  225.      * @throws NullPointerException if the file or encoding is null
  226.      * @throws IOException          in case of an I/O error
  227.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  228.      */
  229.     @Deprecated
  230.     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException {
  231.         this(file, charsetEncoder, false);
  232.     }

  233.     /**
  234.      * Constructs a FileWriterWithEncoding with a file encoding.
  235.      *
  236.      * @param file           the file to write to, not null.
  237.      * @param charsetEncoder the encoding to use, null uses the default Charset.
  238.      * @param append         true if content should be appended, false to overwrite.
  239.      * @throws NullPointerException if the file 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.     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
  245.     public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
  246.         this(initWriter(file, charsetEncoder, append));
  247.     }

  248.     /**
  249.      * Constructs a FileWriterWithEncoding with a file encoding.
  250.      *
  251.      * @param file        the file to write to, not null
  252.      * @param charsetName the name of the requested charset, not null
  253.      * @throws NullPointerException if the file or encoding is null
  254.      * @throws IOException          in case of an I/O error
  255.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  256.      */
  257.     @Deprecated
  258.     public FileWriterWithEncoding(final File file, final String charsetName) throws IOException {
  259.         this(file, charsetName, false);
  260.     }

  261.     /**
  262.      * Constructs a FileWriterWithEncoding with a file encoding.
  263.      *
  264.      * @param file        the file to write to, not null.
  265.      * @param charsetName the name of the requested charset, null uses the default Charset.
  266.      * @param append      true if content should be appended, false to overwrite.
  267.      * @throws NullPointerException if the file is null.
  268.      * @throws IOException          in case of an I/O error.
  269.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  270.      */
  271.     @Deprecated
  272.     @SuppressWarnings("resource") // Call site is responsible for closing a new instance.
  273.     public FileWriterWithEncoding(final File file, final String charsetName, final boolean append) throws IOException {
  274.         this(initWriter(file, charsetName, append));
  275.     }

  276.     private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) {
  277.         super(outputStreamWriter);
  278.     }

  279.     /**
  280.      * Constructs a FileWriterWithEncoding with a file encoding.
  281.      *
  282.      * @param fileName the name of the file to write to, not null
  283.      * @param charset  the charset to use, not null
  284.      * @throws NullPointerException if the file name or encoding 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.     public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException {
  290.         this(new File(fileName), charset, false);
  291.     }

  292.     /**
  293.      * Constructs a FileWriterWithEncoding with a file encoding.
  294.      *
  295.      * @param fileName the name of the file to write to, not null
  296.      * @param charset  the encoding to use, not null
  297.      * @param append   true if content should be appended, false to overwrite
  298.      * @throws NullPointerException if the file name or encoding is null
  299.      * @throws IOException          in case of an I/O error
  300.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  301.      */
  302.     @Deprecated
  303.     public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException {
  304.         this(new File(fileName), charset, append);
  305.     }

  306.     /**
  307.      * Constructs a FileWriterWithEncoding with a file encoding.
  308.      *
  309.      * @param fileName the name of the file to write to, not null
  310.      * @param encoding the encoding to use, not null
  311.      * @throws NullPointerException if the file name or encoding is null
  312.      * @throws IOException          in case of an I/O error
  313.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  314.      */
  315.     @Deprecated
  316.     public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException {
  317.         this(new File(fileName), encoding, false);
  318.     }

  319.     /**
  320.      * Constructs a FileWriterWithEncoding with a file encoding.
  321.      *
  322.      * @param fileName       the name of the file to write to, not null
  323.      * @param charsetEncoder the encoding to use, not null
  324.      * @param append         true if content should be appended, false to overwrite
  325.      * @throws NullPointerException if the file name or encoding is null
  326.      * @throws IOException          in case of an I/O error
  327.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  328.      */
  329.     @Deprecated
  330.     public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException {
  331.         this(new File(fileName), charsetEncoder, append);
  332.     }

  333.     /**
  334.      * Constructs a FileWriterWithEncoding with a file encoding.
  335.      *
  336.      * @param fileName    the name of the file to write to, not null
  337.      * @param charsetName the name of the requested charset, not null
  338.      * @throws NullPointerException if the file name or encoding is null
  339.      * @throws IOException          in case of an I/O error
  340.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  341.      */
  342.     @Deprecated
  343.     public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException {
  344.         this(new File(fileName), charsetName, false);
  345.     }

  346.     /**
  347.      * Constructs a FileWriterWithEncoding with a file encoding.
  348.      *
  349.      * @param fileName    the name of the file to write to, not null
  350.      * @param charsetName the name of the requested charset, not null
  351.      * @param append      true if content should be appended, false to overwrite
  352.      * @throws NullPointerException if the file name or encoding is null
  353.      * @throws IOException          in case of an I/O error
  354.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  355.      */
  356.     @Deprecated
  357.     public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException {
  358.         this(new File(fileName), charsetName, append);
  359.     }
  360. }