LockableFileWriter.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.FileOutputStream;
  20. import java.io.FileWriter;
  21. import java.io.IOException;
  22. import java.io.OutputStreamWriter;
  23. import java.io.UnsupportedEncodingException;
  24. import java.io.Writer;
  25. import java.nio.charset.Charset;
  26. import java.util.Objects;

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

  31. /**
  32.  * FileWriter that will create and honor lock files to allow simple cross thread file lock handling.
  33.  * <p>
  34.  * This class provides a simple alternative to {@link FileWriter} that will use a lock file to prevent duplicate writes.
  35.  * </p>
  36.  * <p>
  37.  * <strong>Note:</strong> The lock file is deleted when {@link #close()} is called - or if the main file cannot be opened initially. In the (unlikely) event
  38.  * that the lock file cannot be deleted, an exception is thrown.
  39.  * </p>
  40.  * <p>
  41.  * By default, the file will be overwritten, but this may be changed to append. The lock directory may be specified, but defaults to the system property
  42.  * {@code java.io.tmpdir}. The encoding may also be specified, and defaults to the platform default.
  43.  * </p>
  44.  * <p>
  45.  * To build an instance, use {@link Builder}.
  46.  * </p>
  47.  *
  48.  * @see Builder
  49.  */
  50. public class LockableFileWriter extends Writer {

  51.     // @formatter:off
  52.     /**
  53.      * Builds a new {@link LockableFileWriter}.
  54.      *
  55.      * <p>
  56.      * Using a CharsetEncoder:
  57.      * </p>
  58.      * <pre>{@code
  59.      * LockableFileWriter w = LockableFileWriter.builder()
  60.      *   .setPath(path)
  61.      *   .setAppend(false)
  62.      *   .setLockDirectory("Some/Directory")
  63.      *   .get();}
  64.      * </pre>
  65.      *
  66.      * @see #get()
  67.      * @since 2.12.0
  68.      */
  69.     // @formatter:on
  70.     public static class Builder extends AbstractStreamBuilder<LockableFileWriter, Builder> {

  71.         private boolean append;
  72.         private AbstractOrigin<?, ?> lockDirectory = newFileOrigin(FileUtils.getTempDirectoryPath());

  73.         /**
  74.          * Constructs a new builder of {@link LockableFileWriter}.
  75.          */
  76.         public Builder() {
  77.             setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
  78.             setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
  79.         }

  80.         private File checkOriginFile() {
  81.             return checkOrigin().getFile();
  82.         }

  83.         /**
  84.          * Constructs a new instance.
  85.          * <p>
  86.          * You must set an aspect that supports {@link File} on this builder, otherwise, this method throws an exception.
  87.          * </p>
  88.          * <p>
  89.          * This builder uses the following aspects:
  90.          * </p>
  91.          * <ul>
  92.          * <li>{@link File} is the target aspect.</li>
  93.          * <li>{@link #getCharset()}</li>
  94.          * <li>append</li>
  95.          * <li>lockDirectory</li>
  96.          * </ul>
  97.          *
  98.          * @return a new instance.
  99.          * @throws UnsupportedOperationException if the origin cannot provide a File.
  100.          * @throws IllegalStateException         if the {@code origin} is {@code null}.
  101.          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link #getFile()}.
  102.          * @see AbstractOrigin#getFile()
  103.          * @see #getUnchecked()
  104.          */
  105.         @Override
  106.         public LockableFileWriter get() throws IOException {
  107.             return new LockableFileWriter(this);
  108.         }

  109.         /**
  110.          * Sets whether to append (true) or overwrite (false).
  111.          *
  112.          * @param append whether to append (true) or overwrite (false).
  113.          * @return {@code this} instance.
  114.          */
  115.         public Builder setAppend(final boolean append) {
  116.             this.append = append;
  117.             return this;
  118.         }

  119.         /**
  120.          * Sets the directory in which the lock file should be held.
  121.          *
  122.          * @param lockDirectory the directory in which the lock file should be held.
  123.          * @return {@code this} instance.
  124.          */
  125.         public Builder setLockDirectory(final File lockDirectory) {
  126.             this.lockDirectory = newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectory());
  127.             return this;
  128.         }

  129.         /**
  130.          * Sets the directory in which the lock file should be held.
  131.          *
  132.          * @param lockDirectory the directory in which the lock file should be held.
  133.          * @return {@code this} instance.
  134.          */
  135.         public Builder setLockDirectory(final String lockDirectory) {
  136.             this.lockDirectory = newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectoryPath());
  137.             return this;
  138.         }

  139.     }

  140.     /** The extension for the lock file. */
  141.     private static final String LCK = ".lck";

  142.     /**
  143.      * Constructs a new {@link Builder}.
  144.      *
  145.      * @return a new {@link Builder}.
  146.      * @since 2.12.0
  147.      */
  148.     public static Builder builder() {
  149.         return new Builder();
  150.     }

  151.     /** The writer to decorate. */
  152.     private final Writer out;

  153.     /** The lock file. */
  154.     private final File lockFile;

  155.     private LockableFileWriter(final Builder builder) throws IOException {
  156.         this(builder.checkOriginFile(), builder.getCharset(), builder.append, builder.lockDirectory.getFile().toString());
  157.     }


  158.     /**
  159.      * Constructs a LockableFileWriter. If the file exists, it is overwritten.
  160.      *
  161.      * @param file the file to write to, not null
  162.      * @throws NullPointerException if the file is null
  163.      * @throws IOException          in case of an I/O error
  164.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  165.      */
  166.     @Deprecated
  167.     public LockableFileWriter(final File file) throws IOException {
  168.         this(file, false, null);
  169.     }

  170.     /**
  171.      * Constructs a LockableFileWriter.
  172.      *
  173.      * @param file   the file to write to, not null
  174.      * @param append true if content should be appended, false to overwrite
  175.      * @throws NullPointerException if the file is null
  176.      * @throws IOException          in case of an I/O error
  177.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  178.      */
  179.     @Deprecated
  180.     public LockableFileWriter(final File file, final boolean append) throws IOException {
  181.         this(file, append, null);
  182.     }

  183.     /**
  184.      * Constructs a LockableFileWriter.
  185.      * <p>
  186.      * The new instance uses the virtual machine's {@link Charset#defaultCharset() default charset}.
  187.      * </p>
  188.      *
  189.      * @param file    the file to write to, not null
  190.      * @param append  true if content should be appended, false to overwrite
  191.      * @param lockDir the directory in which the lock file should be held
  192.      * @throws NullPointerException if the file is null
  193.      * @throws IOException          in case of an I/O error
  194.      * @deprecated Use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
  195.      */
  196.     @Deprecated
  197.     public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
  198.         this(file, Charset.defaultCharset(), append, lockDir);
  199.     }

  200.     /**
  201.      * Constructs a LockableFileWriter with a file encoding.
  202.      *
  203.      * @param file    the file to write to, not null
  204.      * @param charset the charset to use, null means platform default
  205.      * @throws NullPointerException if the file is null
  206.      * @throws IOException          in case of an I/O error
  207.      * @since 2.3
  208.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  209.      */
  210.     @Deprecated
  211.     public LockableFileWriter(final File file, final Charset charset) throws IOException {
  212.         this(file, charset, false, null);
  213.     }

  214.     /**
  215.      * Constructs a LockableFileWriter with a file encoding.
  216.      *
  217.      * @param file    the file to write to, not null
  218.      * @param charset the name of the requested charset, null means platform default
  219.      * @param append  true if content should be appended, false to overwrite
  220.      * @param lockDir the directory in which the lock file should be held
  221.      * @throws NullPointerException if the file is null
  222.      * @throws IOException          in case of an I/O error
  223.      * @since 2.3
  224.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  225.      */
  226.     @Deprecated
  227.     public LockableFileWriter(final File file, final Charset charset, final boolean append, final String lockDir) throws IOException {
  228.         // init file to create/append
  229.         final File absFile = Objects.requireNonNull(file, "file").getAbsoluteFile();
  230.         if (absFile.getParentFile() != null) {
  231.             FileUtils.forceMkdir(absFile.getParentFile());
  232.         }
  233.         if (absFile.isDirectory()) {
  234.             throw new IOException("File specified is a directory");
  235.         }
  236.         // init lock file
  237.         final File lockDirFile = new File(lockDir != null ? lockDir : FileUtils.getTempDirectoryPath());
  238.         FileUtils.forceMkdir(lockDirFile);
  239.         testLockDir(lockDirFile);
  240.         lockFile = new File(lockDirFile, absFile.getName() + LCK);
  241.         // check if locked
  242.         createLock();
  243.         // init wrapped writer
  244.         out = initWriter(absFile, charset, append);
  245.     }

  246.     /**
  247.      * Constructs a LockableFileWriter with a file encoding.
  248.      *
  249.      * @param file        the file to write to, not null
  250.      * @param charsetName the name of the requested charset, null means platform default
  251.      * @throws NullPointerException                         if the file is null
  252.      * @throws IOException                                  in case of an I/O error
  253.      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
  254.      *                                                      supported.
  255.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  256.      */
  257.     @Deprecated
  258.     public LockableFileWriter(final File file, final String charsetName) throws IOException {
  259.         this(file, charsetName, false, null);
  260.     }

  261.     /**
  262.      * Constructs a LockableFileWriter with a file encoding.
  263.      *
  264.      * @param file        the file to write to, not null
  265.      * @param charsetName the encoding to use, null means platform default
  266.      * @param append      true if content should be appended, false to overwrite
  267.      * @param lockDir     the directory in which the lock file should be held
  268.      * @throws NullPointerException                         if the file is null
  269.      * @throws IOException                                  in case of an I/O error
  270.      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
  271.      *                                                      supported.
  272.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  273.      */
  274.     @Deprecated
  275.     public LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir) throws IOException {
  276.         this(file, Charsets.toCharset(charsetName), append, lockDir);
  277.     }

  278.     /**
  279.      * Constructs a LockableFileWriter. If the file exists, it is overwritten.
  280.      *
  281.      * @param fileName the file to write to, not null
  282.      * @throws NullPointerException if the file is null
  283.      * @throws IOException          in case of an I/O error
  284.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  285.      */
  286.     @Deprecated
  287.     public LockableFileWriter(final String fileName) throws IOException {
  288.         this(fileName, false, null);
  289.     }

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

  303.     /**
  304.      * Constructs a LockableFileWriter.
  305.      *
  306.      * @param fileName the file to write to, not null
  307.      * @param append   true if content should be appended, false to overwrite
  308.      * @param lockDir  the directory in which the lock file should be held
  309.      * @throws NullPointerException if the file is null
  310.      * @throws IOException          in case of an I/O error
  311.      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
  312.      */
  313.     @Deprecated
  314.     public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
  315.         this(new File(fileName), append, lockDir);
  316.     }

  317.     /**
  318.      * Closes the file writer and deletes the lock file.
  319.      *
  320.      * @throws IOException if an I/O error occurs.
  321.      */
  322.     @Override
  323.     public void close() throws IOException {
  324.         try {
  325.             out.close();
  326.         } finally {
  327.             FileUtils.delete(lockFile);
  328.         }
  329.     }

  330.     /**
  331.      * Creates the lock file.
  332.      *
  333.      * @throws IOException if we cannot create the file
  334.      */
  335.     private void createLock() throws IOException {
  336.         synchronized (LockableFileWriter.class) {
  337.             if (!lockFile.createNewFile()) {
  338.                 throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists");
  339.             }
  340.             lockFile.deleteOnExit();
  341.         }
  342.     }

  343.     /**
  344.      * Flushes the stream.
  345.      *
  346.      * @throws IOException if an I/O error occurs.
  347.      */
  348.     @Override
  349.     public void flush() throws IOException {
  350.         out.flush();
  351.     }

  352.     /**
  353.      * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
  354.      *
  355.      * @param file    the file to be accessed
  356.      * @param charset the charset to use
  357.      * @param append  true to append
  358.      * @return The initialized writer
  359.      * @throws IOException if an error occurs
  360.      */
  361.     private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
  362.         final boolean fileExistedAlready = file.exists();
  363.         try {
  364.             return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset));
  365.         } catch (final IOException | RuntimeException ex) {
  366.             FileUtils.deleteQuietly(lockFile);
  367.             if (!fileExistedAlready) {
  368.                 FileUtils.deleteQuietly(file);
  369.             }
  370.             throw ex;
  371.         }
  372.     }

  373.     /**
  374.      * Tests that we can write to the lock directory.
  375.      *
  376.      * @param lockDir the File representing the lock directory
  377.      * @throws IOException if we cannot write to the lock directory
  378.      * @throws IOException if we cannot find the lock file
  379.      */
  380.     private void testLockDir(final File lockDir) throws IOException {
  381.         if (!lockDir.exists()) {
  382.             throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath());
  383.         }
  384.         if (!lockDir.canWrite()) {
  385.             throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath());
  386.         }
  387.     }

  388.     /**
  389.      * Writes the characters from an array.
  390.      *
  391.      * @param cbuf the characters to write
  392.      * @throws IOException if an I/O error occurs.
  393.      */
  394.     @Override
  395.     public void write(final char[] cbuf) throws IOException {
  396.         out.write(cbuf);
  397.     }

  398.     /**
  399.      * Writes the specified characters from an array.
  400.      *
  401.      * @param cbuf the characters to write
  402.      * @param off  The start offset
  403.      * @param len  The number of characters to write
  404.      * @throws IOException if an I/O error occurs.
  405.      */
  406.     @Override
  407.     public void write(final char[] cbuf, final int off, final int len) throws IOException {
  408.         out.write(cbuf, off, len);
  409.     }

  410.     /**
  411.      * Writes a character.
  412.      *
  413.      * @param c the character to write
  414.      * @throws IOException if an I/O error occurs.
  415.      */
  416.     @Override
  417.     public void write(final int c) throws IOException {
  418.         out.write(c);
  419.     }

  420.     /**
  421.      * Writes the characters from a string.
  422.      *
  423.      * @param str the string to write
  424.      * @throws IOException if an I/O error occurs.
  425.      */
  426.     @Override
  427.     public void write(final String str) throws IOException {
  428.         out.write(str);
  429.     }

  430.     /**
  431.      * Writes the specified characters from a string.
  432.      *
  433.      * @param str the string to write
  434.      * @param off The start offset
  435.      * @param len The number of characters to write
  436.      * @throws IOException if an I/O error occurs.
  437.      */
  438.     @Override
  439.     public void write(final String str, final int off, final int len) throws IOException {
  440.         out.write(str, off, len);
  441.     }

  442. }