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