    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 *      http://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.FileOutputStream;
    021import java.io.FileWriter;
    022import java.io.IOException;
    023import java.io.OutputStreamWriter;
    024import java.io.Writer;
    025import java.nio.charset.Charset;
    026import java.util.Objects;
    027
    028import org.apache.commons.io.Charsets;
    029import org.apache.commons.io.FileUtils;
    030import org.apache.commons.io.build.AbstractOrigin;
    031import org.apache.commons.io.build.AbstractOriginSupplier;
    032import org.apache.commons.io.build.AbstractStreamBuilder;
    033
    034/**
    035 * FileWriter that will create and honor lock files to allow simple cross thread file lock handling.
    036 * <p>
    037 * This class provides a simple alternative to {@link FileWriter} that will use a lock file to prevent duplicate writes.
    038 * </p>
    039 * <p>
    040 * <b>Note:</b> The lock file is deleted when {@link #close()} is called - or if the main file cannot be opened initially. In the (unlikely) event that the lock
    041 * file cannot be deleted, an exception is thrown.
    042 * </p>
    043 * <p>
    044 * 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
    045 * {@code java.io.tmpdir}. The encoding may also be specified, and defaults to the platform default.
    046 * </p>
    047 * <p>
    048 * To build an instance, see {@link Builder}.
    049 * </p>
    050 */
    051public class LockableFileWriter extends Writer {
    052
    053    /**
    054     * Builds a new {@link LockableFileWriter} instance.
    055     * <p>
    056     * Using a CharsetEncoder:
    057     * </p>
    058     * <pre>{@code
    059     * LockableFileWriter w = LockableFileWriter.builder()
    060     *   .setPath(path)
    061     *   .setAppend(false)
    062     *   .setLockDirectory("Some/Directory")
    063     *   .get();}
    064     * </pre>
    065     *
    066     * @since 2.12.0
    067     */
    068    public static class Builder extends AbstractStreamBuilder<LockableFileWriter, Builder> {
    069
    070        private boolean append;
    071        private AbstractOrigin<?, ?> lockDirectory = AbstractOriginSupplier.newFileOrigin(FileUtils.getTempDirectoryPath());
    072
    073        /**
    074         * Constructs a new Builder.
    075         */
    076        public Builder() {
    077            setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
    078            setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
    079        }
    080
    081        /**
    082         * Constructs a new instance.
    083         * <p>
    084         * This builder use the aspects File, Charset, append, and lockDirectory.
    085         * </p>
    086         * <p>
    087         * You must provide an origin that can be converted to a File by this builder, otherwise, this call will throw an
    088         * {@link UnsupportedOperationException}.
    089         * </p>
    090         *
    091         * @return a new instance.
    092         * @throws UnsupportedOperationException if the origin cannot provide a File.
    093         * @throws IllegalStateException if the {@code origin} is {@code null}.
    094         * @see AbstractOrigin#getFile()
    095         */
    096        @Override
    097        public LockableFileWriter get() throws IOException {
    098            return new LockableFileWriter(checkOrigin().getFile(), getCharset(), append, lockDirectory.getFile().toString());
    099        }
    100
    101        /**
    102         * Sets whether to append (true) or overwrite (false).
    103         *
    104         * @param append whether to append (true) or overwrite (false).
    105         * @return this
    106         */
    107        public Builder setAppend(final boolean append) {
    108            this.append = append;
    109            return this;
    110        }
    111
    112        /**
    113         * Sets the directory in which the lock file should be held.
    114         *
    115         * @param lockDirectory the directory in which the lock file should be held.
    116         * @return this
    117         */
    118        public Builder setLockDirectory(final File lockDirectory) {
    119            this.lockDirectory = AbstractOriginSupplier.newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectory());
    120            return this;
    121        }
    122
    123        /**
    124         * Sets the directory in which the lock file should be held.
    125         *
    126         * @param lockDirectory the directory in which the lock file should be held.
    127         * @return this
    128         */
    129        public Builder setLockDirectory(final String lockDirectory) {
    130            this.lockDirectory = AbstractOriginSupplier.newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectoryPath());
    131            return this;
    132        }
    133
    134    }
    135
    136    /** The extension for the lock file. */
    137    private static final String LCK = ".lck";
    138
    139    // Cannot extend ProxyWriter, as requires writer to be
    140    // known when super() is called
    141
    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
    152    /** The writer to decorate. */
    153    private final Writer out;
    154
    155    /** The lock file. */
    156    private final File lockFile;
    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    /**
    172     * Constructs a LockableFileWriter.
    173     *
    174     * @param file   the file to write to, not null
    175     * @param append true if content should be appended, false to overwrite
    176     * @throws NullPointerException if the file is null
    177     * @throws IOException          in case of an I/O error
    178     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    179     */
    180    @Deprecated
    181    public LockableFileWriter(final File file, final boolean append) throws IOException {
    182        this(file, append, null);
    183    }
    184
    185    /**
    186     * Constructs a LockableFileWriter.
    187     *
    188     * @param file    the file to write to, not null
    189     * @param append  true if content should be appended, false to overwrite
    190     * @param lockDir the directory in which the lock file should be held
    191     * @throws NullPointerException if the file is null
    192     * @throws IOException          in case of an I/O error
    193     * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
    194     */
    195    @Deprecated
    196    public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
    197        this(file, Charset.defaultCharset(), append, lockDir);
    198    }
    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    /**
    216     * Constructs a LockableFileWriter with a file encoding.
    217     *
    218     * @param file    the file to write to, not null
    219     * @param charset the name of the requested charset, null means platform default
    220     * @param append  true if content should be appended, false to overwrite
    221     * @param lockDir the directory in which the lock file should be held
    222     * @throws NullPointerException if the file is null
    223     * @throws IOException          in case of an I/O error
    224     * @since 2.3
    225     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    226     */
    227    @Deprecated
    228    public LockableFileWriter(final File file, final Charset charset, final boolean append, final String lockDir) throws IOException {
    229        // init file to create/append
    230        final File absFile = Objects.requireNonNull(file, "file").getAbsoluteFile();
    231        if (absFile.getParentFile() != null) {
    232            FileUtils.forceMkdir(absFile.getParentFile());
    233        }
    234        if (absFile.isDirectory()) {
    235            throw new IOException("File specified is a directory");
    236        }
    237
    238        // init lock file
    239        final File lockDirFile = new File(lockDir != null ? lockDir : FileUtils.getTempDirectoryPath());
    240        FileUtils.forceMkdir(lockDirFile);
    241        testLockDir(lockDirFile);
    242        lockFile = new File(lockDirFile, absFile.getName() + LCK);
    243
    244        // check if locked
    245        createLock();
    246
    247        // init wrapped writer
    248        out = initWriter(absFile, charset, append);
    249    }
    250
    251    /**
    252     * Constructs a LockableFileWriter with a file encoding.
    253     *
    254     * @param file        the file to write to, not null
    255     * @param charsetName the name of the requested charset, null means platform default
    256     * @throws NullPointerException                         if the file is null
    257     * @throws IOException                                  in case of an I/O error
    258     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    259     *                                                      supported.
    260     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    261     */
    262    @Deprecated
    263    public LockableFileWriter(final File file, final String charsetName) throws IOException {
    264        this(file, charsetName, false, null);
    265    }
    266
    267    /**
    268     * Constructs a LockableFileWriter with a file encoding.
    269     *
    270     * @param file        the file to write to, not null
    271     * @param charsetName the encoding to use, null means platform default
    272     * @param append      true if content should be appended, false to overwrite
    273     * @param lockDir     the directory in which the lock file should be held
    274     * @throws NullPointerException                         if the file is null
    275     * @throws IOException                                  in case of an I/O error
    276     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    277     *                                                      supported.
    278     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    279     */
    280    @Deprecated
    281    public LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir) throws IOException {
    282        this(file, Charsets.toCharset(charsetName), append, lockDir);
    283    }
    284
    285    /**
    286     * Constructs a LockableFileWriter. If the file exists, it is overwritten.
    287     *
    288     * @param fileName the file to write to, not null
    289     * @throws NullPointerException if the file is null
    290     * @throws IOException          in case of an I/O error
    291     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    292     */
    293    @Deprecated
    294    public LockableFileWriter(final String fileName) throws IOException {
    295        this(fileName, false, null);
    296    }
    297
    298    /**
    299     * Constructs a LockableFileWriter.
    300     *
    301     * @param fileName file to write to, not null
    302     * @param append   true if content should be appended, false to overwrite
    303     * @throws NullPointerException if the file 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 LockableFileWriter(final String fileName, final boolean append) throws IOException {
    309        this(fileName, append, null);
    310    }
    311
    312    /**
    313     * Constructs a LockableFileWriter.
    314     *
    315     * @param fileName the file to write to, not null
    316     * @param append   true if content should be appended, false to overwrite
    317     * @param lockDir  the directory in which the lock file should be held
    318     * @throws NullPointerException if the file 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 LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
    324        this(new File(fileName), append, lockDir);
    325    }
    326
    327    /**
    328     * Closes the file writer and deletes the lock file.
    329     *
    330     * @throws IOException if an I/O error occurs.
    331     */
    332    @Override
    333    public void close() throws IOException {
    334        try {
    335            out.close();
    336        } finally {
    337            FileUtils.delete(lockFile);
    338        }
    339    }
    340
    341    /**
    342     * Creates the lock file.
    343     *
    344     * @throws IOException if we cannot create the file
    345     */
    346    private void createLock() throws IOException {
    347        synchronized (LockableFileWriter.class) {
    348            if (!lockFile.createNewFile()) {
    349                throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists");
    350            }
    351            lockFile.deleteOnExit();
    352        }
    353    }
    354
    355    /**
    356     * Flushes the stream.
    357     *
    358     * @throws IOException if an I/O error occurs.
    359     */
    360    @Override
    361    public void flush() throws IOException {
    362        out.flush();
    363    }
    364
    365    /**
    366     * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
    367     *
    368     * @param file    the file to be accessed
    369     * @param charset the charset to use
    370     * @param append  true to append
    371     * @return The initialized writer
    372     * @throws IOException if an error occurs
    373     */
    374    private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
    375        final boolean fileExistedAlready = file.exists();
    376        try {
    377            return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset));
    378
    379        } catch (final IOException | RuntimeException ex) {
    380            FileUtils.deleteQuietly(lockFile);
    381            if (!fileExistedAlready) {
    382                FileUtils.deleteQuietly(file);
    383            }
    384            throw ex;
    385        }
    386    }
    387
    388    /**
    389     * Tests that we can write to the lock directory.
    390     *
    391     * @param lockDir the File representing the lock directory
    392     * @throws IOException if we cannot write to the lock directory
    393     * @throws IOException if we cannot find the lock file
    394     */
    395    private void testLockDir(final File lockDir) throws IOException {
    396        if (!lockDir.exists()) {
    397            throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath());
    398        }
    399        if (!lockDir.canWrite()) {
    400            throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath());
    401        }
    402    }
    403
    404    /**
    405     * Writes the characters from an array.
    406     *
    407     * @param cbuf the characters to write
    408     * @throws IOException if an I/O error occurs.
    409     */
    410    @Override
    411    public void write(final char[] cbuf) throws IOException {
    412        out.write(cbuf);
    413    }
    414
    415    /**
    416     * Writes the specified characters from an array.
    417     *
    418     * @param cbuf the characters to write
    419     * @param off  The start offset
    420     * @param len  The number of characters to write
    421     * @throws IOException if an I/O error occurs.
    422     */
    423    @Override
    424    public void write(final char[] cbuf, final int off, final int len) throws IOException {
    425        out.write(cbuf, off, len);
    426    }
    427
    428    /**
    429     * Writes a character.
    430     *
    431     * @param c the character to write
    432     * @throws IOException if an I/O error occurs.
    433     */
    434    @Override
    435    public void write(final int c) throws IOException {
    436        out.write(c);
    437    }
    438
    439    /**
    440     * Writes the characters from a string.
    441     *
    442     * @param str the string to write
    443     * @throws IOException if an I/O error occurs.
    444     */
    445    @Override
    446    public void write(final String str) throws IOException {
    447        out.write(str);
    448    }
    449
    450    /**
    451     * Writes the specified characters from a string.
    452     *
    453     * @param str the string to write
    454     * @param off The start offset
    455     * @param len The number of characters to write
    456     * @throws IOException if an I/O error occurs.
    457     */
    458    @Override
    459    public void write(final String str, final int off, final int len) throws IOException {
    460        out.write(str, off, len);
    461    }
    462
    463}