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