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