    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</code>
    034 * that will use a lock file to prevent duplicate writes.
    035 * <p>
    036 * <b>N.B.</b> the lock file is deleted when {@link #close()} is called
    037 * - or if the main file cannot be opened initially.
    038 * In the (unlikely) event that the lockfile cannot be deleted,
    039 * this is not reported, and subsequent requests using
    040 * the same lockfile will fail.
    041 * <p>
    042 * By default, the file will be overwritten, but this may be changed to append.
    043 * The lock directory may be specified, but defaults to the system property
    044 * <code>java.io.tmpdir</code>.
    045 * The encoding may also be specified, and defaults to the platform default.
    046 *
    047 */
    048public class LockableFileWriter extends Writer {
    049    // Cannot extend ProxyWriter, as requires writer to be
    050    // known when super() is called
    051
    052    /** The extension for the lock file. */
    053    private static final String LCK = ".lck";
    054
    055    /** The writer to decorate. */
    056    private final Writer out;
    057    /** The lock file. */
    058    private final File lockFile;
    059
    060    /**
    061     * Constructs a LockableFileWriter.
    062     * If the file exists, it is overwritten.
    063     *
    064     * @param fileName  the file to write to, not null
    065     * @throws NullPointerException if the file is null
    066     * @throws IOException in case of an I/O error
    067     */
    068    public LockableFileWriter(final String fileName) throws IOException {
    069        this(fileName, false, null);
    070    }
    071
    072    /**
    073     * Constructs a LockableFileWriter.
    074     *
    075     * @param fileName  file to write to, not null
    076     * @param append  true if content should be appended, false to overwrite
    077     * @throws NullPointerException if the file is null
    078     * @throws IOException in case of an I/O error
    079     */
    080    public LockableFileWriter(final String fileName, final boolean append) throws IOException {
    081        this(fileName, append, null);
    082    }
    083
    084    /**
    085     * Constructs a LockableFileWriter.
    086     *
    087     * @param fileName  the file to write to, not null
    088     * @param append  true if content should be appended, false to overwrite
    089     * @param lockDir  the directory in which the lock file should be held
    090     * @throws NullPointerException if the file is null
    091     * @throws IOException in case of an I/O error
    092     */
    093    public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
    094        this(new File(fileName), append, lockDir);
    095    }
    096
    097    /**
    098     * Constructs a LockableFileWriter.
    099     * If the file exists, it is overwritten.
    100     *
    101     * @param file  the file to write to, not null
    102     * @throws NullPointerException if the file is null
    103     * @throws IOException in case of an I/O error
    104     */
    105    public LockableFileWriter(final File file) throws IOException {
    106        this(file, false, null);
    107    }
    108
    109    /**
    110     * Constructs a LockableFileWriter.
    111     *
    112     * @param file  the file to write to, not null
    113     * @param append  true if content should be appended, false to overwrite
    114     * @throws NullPointerException if the file is null
    115     * @throws IOException in case of an I/O error
    116     */
    117    public LockableFileWriter(final File file, final boolean append) throws IOException {
    118        this(file, append, null);
    119    }
    120
    121    /**
    122     * Constructs a LockableFileWriter.
    123     *
    124     * @param file  the file to write to, not null
    125     * @param append  true if content should be appended, false to overwrite
    126     * @param lockDir  the directory in which the lock file should be held
    127     * @throws NullPointerException if the file is null
    128     * @throws IOException in case of an I/O error
    129     * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
    130     */
    131    @Deprecated
    132    public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
    133        this(file, Charset.defaultCharset(), append, lockDir);
    134    }
    135
    136    /**
    137     * Constructs a LockableFileWriter with a file encoding.
    138     *
    139     * @param file  the file to write to, not null
    140     * @param charset  the charset to use, null means platform default
    141     * @throws NullPointerException if the file is null
    142     * @throws IOException in case of an I/O error
    143     * @since 2.3
    144     */
    145    public LockableFileWriter(final File file, final Charset charset) throws IOException {
    146        this(file, charset, false, null);
    147    }
    148
    149    /**
    150     * Constructs a LockableFileWriter with a file encoding.
    151     *
    152     * @param file  the file to write to, not null
    153     * @param charsetName  the name of the requested charset, null means platform default
    154     * @throws NullPointerException if the file is null
    155     * @throws IOException in case of an I/O error
    156     * @throws java.nio.charset.UnsupportedCharsetException
    157     *             thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    158     *             supported.
    159     */
    160    public LockableFileWriter(final File file, final String charsetName) throws IOException {
    161        this(file, charsetName, false, null);
    162    }
    163
    164    /**
    165     * Constructs a LockableFileWriter with a file encoding.
    166     *
    167     * @param file  the file to write to, not null
    168     * @param charset  the name of the requested charset, null means platform default
    169     * @param append  true if content should be appended, false to overwrite
    170     * @param lockDir  the directory in which the lock file should be held
    171     * @throws NullPointerException if the file is null
    172     * @throws IOException in case of an I/O error
    173     * @since 2.3
    174     */
    175    public LockableFileWriter(File file, final Charset charset, final boolean append,
    176            String lockDir) throws IOException {
    177        super();
    178        // init file to create/append
    179        file = file.getAbsoluteFile();
    180        if (file.getParentFile() != null) {
    181            FileUtils.forceMkdir(file.getParentFile());
    182        }
    183        if (file.isDirectory()) {
    184            throw new IOException("File specified is a directory");
    185        }
    186
    187        // init lock file
    188        if (lockDir == null) {
    189            lockDir = System.getProperty("java.io.tmpdir");
    190        }
    191        final File lockDirFile = new File(lockDir);
    192        FileUtils.forceMkdir(lockDirFile);
    193        testLockDir(lockDirFile);
    194        lockFile = new File(lockDirFile, file.getName() + LCK);
    195
    196        // check if locked
    197        createLock();
    198
    199        // init wrapped writer
    200        out = initWriter(file, charset, append);
    201    }
    202
    203    /**
    204     * Constructs a LockableFileWriter with a file encoding.
    205     *
    206     * @param file  the file to write to, not null
    207     * @param charsetName  the encoding to use, null means platform default
    208     * @param append  true if content should be appended, false to overwrite
    209     * @param lockDir  the directory in which the lock file should be held
    210     * @throws NullPointerException if the file is null
    211     * @throws IOException in case of an I/O error
    212     * @throws java.nio.charset.UnsupportedCharsetException
    213     *             thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
    214     *             supported.
    215     */
    216    public LockableFileWriter(final File file, final String charsetName, final boolean append,
    217            final String lockDir) throws IOException {
    218        this(file, Charsets.toCharset(charsetName), append, lockDir);
    219    }
    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     * Initialise 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 initialised 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 == false) {
    274                FileUtils.deleteQuietly(file);
    275            }
    276            throw ex;
    277        }
    278    }
    279
    280    //-----------------------------------------------------------------------
    281    /**
    282     * Closes the file writer and deletes the lockfile (if possible).
    283     *
    284     * @throws IOException if an I/O error occurs
    285     */
    286    @Override
    287    public void close() throws IOException {
    288        try {
    289            out.close();
    290        } finally {
    291            lockFile.delete();
    292        }
    293    }
    294
    295    //-----------------------------------------------------------------------
    296    /**
    297     * Write a character.
    298     * @param idx the character to write
    299     * @throws IOException if an I/O error occurs
    300     */
    301    @Override
    302    public void write(final int idx) throws IOException {
    303        out.write(idx);
    304    }
    305
    306    /**
    307     * Write the characters from an array.
    308     * @param chr the characters to write
    309     * @throws IOException if an I/O error occurs
    310     */
    311    @Override
    312    public void write(final char[] chr) throws IOException {
    313        out.write(chr);
    314    }
    315
    316    /**
    317     * Write the specified characters from an array.
    318     * @param chr the characters to write
    319     * @param st The start offset
    320     * @param end The number of characters to write
    321     * @throws IOException if an I/O error occurs
    322     */
    323    @Override
    324    public void write(final char[] chr, final int st, final int end) throws IOException {
    325        out.write(chr, st, end);
    326    }
    327
    328    /**
    329     * Write the characters from a string.
    330     * @param str the string to write
    331     * @throws IOException if an I/O error occurs
    332     */
    333    @Override
    334    public void write(final String str) throws IOException {
    335        out.write(str);
    336    }
    337
    338    /**
    339     * Write the specified characters from a string.
    340     * @param str the string to write
    341     * @param st The start offset
    342     * @param end The number of characters to write
    343     * @throws IOException if an I/O error occurs
    344     */
    345    @Override
    346    public void write(final String str, final int st, final int end) throws IOException {
    347        out.write(str, st, end);
    348    }
    349
    350    /**
    351     * Flush the stream.
    352     * @throws IOException if an I/O error occurs
    353     */
    354    @Override
    355    public void flush() throws IOException {
    356        out.flush();
    357    }
    358
    359}