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