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