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