    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.IOException;
    022import java.io.OutputStreamWriter;
    023import java.io.Writer;
    024import java.nio.charset.Charset;
    025
    026import org.apache.commons.io.Charsets;
    027import org.apache.commons.io.FileUtils;
    028
    029/**
    030 * FileWriter that will create and honor lock files to allow simple
    031 * cross thread file lock handling.
    032 * <p>
    033 * This class provides a simple alternative to {@code FileWriter}
    034 * that will use a lock file to prevent duplicate writes.
    035 * </p>
    036 * <p>
    037 * <b>Note:</b> The lock file is deleted when {@link #close()} is called
    038 * - or if the main file cannot be opened initially.
    039 * In the (unlikely) event that the lock file cannot be deleted,
    040 * an exception is thrown.
    041 * </p>
    042 * <p>
    043 * By default, the file will be overwritten, but this may be changed to append.
    044 * The lock directory may be specified, but defaults to the system property
    045 * {@code java.io.tmpdir}.
    046 * The encoding may also be specified, and defaults to the platform default.
    047 * </p>
    048 */
    049public class LockableFileWriter extends Writer {
    050    // Cannot extend ProxyWriter, as requires writer to be
    051    // known when super() is called
    052
    053    /** The extension for the lock file. */
    054    private static final String LCK = ".lck";
    055
    056    /** The writer to decorate. */
    057    private final Writer out;
    058
    059    /** The lock file. */
    060    private final File lockFile;
    061
    062    /**
    063     * Constructs a LockableFileWriter.
    064     * If the file exists, it is overwritten.
    065     *
    066     * @param fileName  the file to write to, not null
    067     * @throws NullPointerException if the file is null
    068     * @throws IOException in case of an I/O error
    069     */
    070    public LockableFileWriter(final String fileName) throws IOException {
    071        this(fileName, false, null);
    072    }
    073
    074    /**
    075     * Constructs a LockableFileWriter.
    076     *
    077     * @param fileName  file to write to, not null
    078     * @param append  true if content should be appended, false to overwrite
    079     * @throws NullPointerException if the file is null
    080     * @throws IOException in case of an I/O error
    081     */
    082    public LockableFileWriter(final String fileName, final boolean append) throws IOException {
    083        this(fileName, append, null);
    084    }
    085
    086    /**
    087     * Constructs a LockableFileWriter.
    088     *
    089     * @param fileName  the file to write to, not null
    090     * @param append  true if content should be appended, false to overwrite
    091     * @param lockDir  the directory in which the lock file should be held
    092     * @throws NullPointerException if the file is null
    093     * @throws IOException in case of an I/O error
    094     */
    095    public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
    096        this(new File(fileName), append, lockDir);
    097    }
    098
    099    /**
    100     * Constructs a LockableFileWriter.
    101     * If the file exists, it is overwritten.
    102     *
    103     * @param file  the file to write to, not null
    104     * @throws NullPointerException if the file is null
    105     * @throws IOException in case of an I/O error
    106     */
    107    public LockableFileWriter(final File file) throws IOException {
    108        this(file, false, null);
    109    }
    110
    111    /**
    112     * Constructs a LockableFileWriter.
    113     *
    114     * @param file  the file to write to, not null
    115     * @param append  true if content should be appended, false to overwrite
    116     * @throws NullPointerException if the file is null
    117     * @throws IOException in case of an I/O error
    118     */
    119    public LockableFileWriter(final File file, final boolean append) throws IOException {
    120        this(file, append, null);
    121    }
    122
    123    /**
    124     * Constructs a LockableFileWriter.
    125     *
    126     * @param file  the file to write to, not null
    127     * @param append  true if content should be appended, false to overwrite
    128     * @param lockDir  the directory in which the lock file should be held
    129     * @throws NullPointerException if the file is null
    130     * @throws IOException in case of an I/O error
    131     * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
    132     */
    133    @Deprecated
    134    public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
    135        this(file, Charset.defaultCharset(), append, lockDir);
    136    }
    137
    138    /**
    139     * Constructs a LockableFileWriter with a file encoding.
    140     *
    141     * @param file  the file to write to, not null
    142     * @param charset  the charset to use, null means platform default
    143     * @throws NullPointerException if the file is null
    144     * @throws IOException in case of an I/O error
    145     * @since 2.3
    146     */
    147    public LockableFileWriter(final File file, final Charset charset) throws IOException {
    148        this(file, charset, false, null);
    149    }
    150
    151    /**
    152     * Constructs a LockableFileWriter with a file encoding.
    153     *
    154     * @param file  the file to write to, not null
    155     * @param charsetName  the name of the requested charset, null means platform default
    156     * @throws NullPointerException if the file is null
    157     * @throws IOException in case of an I/O error
    158     * @throws java.nio.charset.UnsupportedCharsetException
    159     *             thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    160     *             supported.
    161     */
    162    public LockableFileWriter(final File file, final String charsetName) throws IOException {
    163        this(file, charsetName, false, null);
    164    }
    165
    166    /**
    167     * Constructs a LockableFileWriter with a file encoding.
    168     *
    169     * @param file  the file to write to, not null
    170     * @param charset  the name of the requested charset, null means platform default
    171     * @param append  true if content should be appended, false to overwrite
    172     * @param lockDir  the directory in which the lock file should be held
    173     * @throws NullPointerException if the file is null
    174     * @throws IOException in case of an I/O error
    175     * @since 2.3
    176     */
    177    public LockableFileWriter(File file, final Charset charset, final boolean append,
    178            String lockDir) throws IOException {
    179        // init file to create/append
    180        file = file.getAbsoluteFile();
    181        if (file.getParentFile() != null) {
    182            FileUtils.forceMkdir(file.getParentFile());
    183        }
    184        if (file.isDirectory()) {
    185            throw new IOException("File specified is a directory");
    186        }
    187
    188        // init lock file
    189        if (lockDir == null) {
    190            lockDir = System.getProperty("java.io.tmpdir");
    191        }
    192        final File lockDirFile = new File(lockDir);
    193        FileUtils.forceMkdir(lockDirFile);
    194        testLockDir(lockDirFile);
    195        lockFile = new File(lockDirFile, file.getName() + LCK);
    196
    197        // check if locked
    198        createLock();
    199
    200        // init wrapped writer
    201        out = initWriter(file, charset, append);
    202    }
    203
    204    /**
    205     * Constructs a LockableFileWriter with a file encoding.
    206     *
    207     * @param file  the file to write to, not null
    208     * @param charsetName  the encoding to use, null means platform default
    209     * @param append  true if content should be appended, false to overwrite
    210     * @param lockDir  the directory in which the lock file should be held
    211     * @throws NullPointerException if the file is null
    212     * @throws IOException in case of an I/O error
    213     * @throws java.nio.charset.UnsupportedCharsetException
    214     *             thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    215     *             supported.
    216     */
    217    public LockableFileWriter(final File file, final String charsetName, final boolean append,
    218            final String lockDir) throws IOException {
    219        this(file, Charsets.toCharset(charsetName), append, lockDir);
    220    }
    221
    222    /**
    223     * Tests that we can write to the lock directory.
    224     *
    225     * @param lockDir  the File representing the lock directory
    226     * @throws IOException if we cannot write to the lock directory
    227     * @throws IOException if we cannot find the lock file
    228     */
    229    private void testLockDir(final File lockDir) throws IOException {
    230        if (!lockDir.exists()) {
    231            throw new IOException(
    232                    "Could not find lockDir: " + lockDir.getAbsolutePath());
    233        }
    234        if (!lockDir.canWrite()) {
    235            throw new IOException(
    236                    "Could not write to lockDir: " + lockDir.getAbsolutePath());
    237        }
    238    }
    239
    240    /**
    241     * Creates the lock file.
    242     *
    243     * @throws IOException if we cannot create the file
    244     */
    245    private void createLock() throws IOException {
    246        synchronized (LockableFileWriter.class) {
    247            if (!lockFile.createNewFile()) {
    248                throw new IOException("Can't write file, lock " +
    249                        lockFile.getAbsolutePath() + " exists");
    250            }
    251            lockFile.deleteOnExit();
    252        }
    253    }
    254
    255    /**
    256     * Initializes the wrapped file writer.
    257     * Ensure that a cleanup occurs if the writer creation fails.
    258     *
    259     * @param file  the file to be accessed
    260     * @param charset  the charset to use
    261     * @param append  true to append
    262     * @return The initialized writer
    263     * @throws IOException if an error occurs
    264     */
    265    private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
    266        final boolean fileExistedAlready = file.exists();
    267        try {
    268            return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append),
    269                                          Charsets.toCharset(charset));
    270
    271        } catch (final IOException | RuntimeException ex) {
    272            FileUtils.deleteQuietly(lockFile);
    273            if (!fileExistedAlready) {
    274                FileUtils.deleteQuietly(file);
    275            }
    276            throw ex;
    277        }
    278    }
    279
    280    /**
    281     * Closes the file writer and deletes the lock file.
    282     *
    283     * @throws IOException if an I/O error occurs.
    284     */
    285    @Override
    286    public void close() throws IOException {
    287        try {
    288            out.close();
    289        } finally {
    290            FileUtils.delete(lockFile);
    291        }
    292    }
    293
    294    /**
    295     * Writes a character.
    296     * @param c the character to write
    297     * @throws IOException if an I/O error occurs.
    298     */
    299    @Override
    300    public void write(final int c) throws IOException {
    301        out.write(c);
    302    }
    303
    304    /**
    305     * Writes the characters from an array.
    306     * @param cbuf the characters to write
    307     * @throws IOException if an I/O error occurs.
    308     */
    309    @Override
    310    public void write(final char[] cbuf) throws IOException {
    311        out.write(cbuf);
    312    }
    313
    314    /**
    315     * Writes the specified characters from an array.
    316     * @param cbuf the characters to write
    317     * @param off The start offset
    318     * @param len The number of characters to write
    319     * @throws IOException if an I/O error occurs.
    320     */
    321    @Override
    322    public void write(final char[] cbuf, final int off, final int len) throws IOException {
    323        out.write(cbuf, off, len);
    324    }
    325
    326    /**
    327     * Writes the characters from a string.
    328     * @param str the string to write
    329     * @throws IOException if an I/O error occurs.
    330     */
    331    @Override
    332    public void write(final String str) throws IOException {
    333        out.write(str);
    334    }
    335
    336    /**
    337     * Writes the specified characters from a string.
    338     * @param str the string to write
    339     * @param off The start offset
    340     * @param len The number of characters to write
    341     * @throws IOException if an I/O error occurs.
    342     */
    343    @Override
    344    public void write(final String str, final int off, final int len) throws IOException {
    345        out.write(str, off, len);
    346    }
    347
    348    /**
    349     * Flushes the stream.
    350     * @throws IOException if an I/O error occurs.
    351     */
    352    @Override
    353    public void flush() throws IOException {
    354        out.flush();
    355    }
    356
    357}