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