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