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}