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