• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package org.apache.commons.io.output;
18 
19 import java.io.File;
20 import java.io.FileOutputStream;
21 import java.io.FileWriter;
22 import java.io.IOException;
23 import java.io.OutputStreamWriter;
24 import java.io.Writer;
25 import java.nio.charset.Charset;
26 
27 import org.apache.commons.io.Charsets;
28 import org.apache.commons.io.FileUtils;
29 
30 /**
31  * FileWriter that will create and honor lock files to allow simple
32  * cross thread file lock handling.
33  * <p>
34  * This class provides a simple alternative to {@link FileWriter}
35  * that will use a lock file to prevent duplicate writes.
36  * </p>
37  * <p>
38  * <b>Note:</b> The lock file is deleted when {@link #close()} is called
39  * - or if the main file cannot be opened initially.
40  * In the (unlikely) event that the lock file cannot be deleted,
41  * an exception is thrown.
42  * </p>
43  * <p>
44  * By default, the file will be overwritten, but this may be changed to append.
45  * The lock directory may be specified, but defaults to the system property
46  * {@code java.io.tmpdir}.
47  * The encoding may also be specified, and defaults to the platform default.
48  * </p>
49  */
50 public class LockableFileWriter extends Writer {
51     // Cannot extend ProxyWriter, as requires writer to be
52     // known when super() is called
53 
54     /** The extension for the lock file. */
55     private static final String LCK = ".lck";
56 
57     /** The writer to decorate. */
58     private final Writer out;
59 
60     /** The lock file. */
61     private final File lockFile;
62 
63     /**
64      * Constructs a LockableFileWriter.
65      * If the file exists, it is overwritten.
66      *
67      * @param file  the file to write to, not null
68      * @throws NullPointerException if the file is null
69      * @throws IOException in case of an I/O error
70      */
LockableFileWriter(final File file)71     public LockableFileWriter(final File file) throws IOException {
72         this(file, false, null);
73     }
74 
75     /**
76      * Constructs a LockableFileWriter.
77      *
78      * @param file  the file to write to, not null
79      * @param append  true if content should be appended, false to overwrite
80      * @throws NullPointerException if the file is null
81      * @throws IOException in case of an I/O error
82      */
LockableFileWriter(final File file, final boolean append)83     public LockableFileWriter(final File file, final boolean append) throws IOException {
84         this(file, append, null);
85     }
86 
87     /**
88      * Constructs a LockableFileWriter.
89      *
90      * @param file  the file to write to, not null
91      * @param append  true if content should be appended, false to overwrite
92      * @param lockDir  the directory in which the lock file should be held
93      * @throws NullPointerException if the file is null
94      * @throws IOException in case of an I/O error
95      * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
96      */
97     @Deprecated
LockableFileWriter(final File file, final boolean append, final String lockDir)98     public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
99         this(file, Charset.defaultCharset(), append, lockDir);
100     }
101 
102     /**
103      * Constructs a LockableFileWriter with a file encoding.
104      *
105      * @param file  the file to write to, not null
106      * @param charset  the charset to use, null means platform default
107      * @throws NullPointerException if the file is null
108      * @throws IOException in case of an I/O error
109      * @since 2.3
110      */
LockableFileWriter(final File file, final Charset charset)111     public LockableFileWriter(final File file, final Charset charset) throws IOException {
112         this(file, charset, false, null);
113     }
114 
115     /**
116      * Constructs a LockableFileWriter with a file encoding.
117      *
118      * @param file  the file to write to, not null
119      * @param charset  the name of the requested charset, null means platform default
120      * @param append  true if content should be appended, false to overwrite
121      * @param lockDir  the directory in which the lock file should be held
122      * @throws NullPointerException if the file is null
123      * @throws IOException in case of an I/O error
124      * @since 2.3
125      */
LockableFileWriter(final File file, final Charset charset, final boolean append, String lockDir)126     public LockableFileWriter(final File file, final Charset charset, final boolean append, String lockDir) throws IOException {
127         // init file to create/append
128         final File absFile = file.getAbsoluteFile();
129         if (absFile.getParentFile() != null) {
130             FileUtils.forceMkdir(absFile.getParentFile());
131         }
132         if (absFile.isDirectory()) {
133             throw new IOException("File specified is a directory");
134         }
135 
136         // init lock file
137         if (lockDir == null) {
138             lockDir = FileUtils.getTempDirectoryPath();
139         }
140         final File lockDirFile = new File(lockDir);
141         FileUtils.forceMkdir(lockDirFile);
142         testLockDir(lockDirFile);
143         lockFile = new File(lockDirFile, absFile.getName() + LCK);
144 
145         // check if locked
146         createLock();
147 
148         // init wrapped writer
149         out = initWriter(absFile, charset, append);
150     }
151 
152     /**
153      * Constructs a LockableFileWriter with a file encoding.
154      *
155      * @param file  the file to write to, not null
156      * @param charsetName  the name of the requested charset, 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      */
LockableFileWriter(final File file, final String charsetName)163     public LockableFileWriter(final File file, final String charsetName) throws IOException {
164         this(file, charsetName, 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 charsetName  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      * @throws java.nio.charset.UnsupportedCharsetException
177      *             thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
178      *             supported.
179      */
LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir)180     public LockableFileWriter(final File file, final String charsetName, final boolean append,
181             final String lockDir) throws IOException {
182         this(file, Charsets.toCharset(charsetName), append, lockDir);
183     }
184 
185     /**
186      * Constructs a LockableFileWriter.
187      * If the file exists, it is overwritten.
188      *
189      * @param fileName  the file to write to, not null
190      * @throws NullPointerException if the file is null
191      * @throws IOException in case of an I/O error
192      */
LockableFileWriter(final String fileName)193     public LockableFileWriter(final String fileName) throws IOException {
194         this(fileName, false, null);
195     }
196 
197     /**
198      * Constructs a LockableFileWriter.
199      *
200      * @param fileName  file to write to, not null
201      * @param append  true if content should be appended, false to overwrite
202      * @throws NullPointerException if the file is null
203      * @throws IOException in case of an I/O error
204      */
LockableFileWriter(final String fileName, final boolean append)205     public LockableFileWriter(final String fileName, final boolean append) throws IOException {
206         this(fileName, append, null);
207     }
208 
209     /**
210      * Constructs a LockableFileWriter.
211      *
212      * @param fileName  the file to write to, not null
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      */
LockableFileWriter(final String fileName, final boolean append, final String lockDir)218     public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
219         this(new File(fileName), append, lockDir);
220     }
221 
222     /**
223      * Closes the file writer and deletes the lock file.
224      *
225      * @throws IOException if an I/O error occurs.
226      */
227     @Override
close()228     public void close() throws IOException {
229         try {
230             out.close();
231         } finally {
232             FileUtils.delete(lockFile);
233         }
234     }
235 
236     /**
237      * Creates the lock file.
238      *
239      * @throws IOException if we cannot create the file
240      */
createLock()241     private void createLock() throws IOException {
242         synchronized (LockableFileWriter.class) {
243             if (!lockFile.createNewFile()) {
244                 throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists");
245             }
246             lockFile.deleteOnExit();
247         }
248     }
249 
250     /**
251      * Flushes the stream.
252      * @throws IOException if an I/O error occurs.
253      */
254     @Override
flush()255     public void flush() throws IOException {
256         out.flush();
257     }
258 
259     /**
260      * Initializes the wrapped file writer.
261      * Ensure that a cleanup occurs if the writer creation fails.
262      *
263      * @param file  the file to be accessed
264      * @param charset  the charset to use
265      * @param append  true to append
266      * @return The initialized writer
267      * @throws IOException if an error occurs
268      */
initWriter(final File file, final Charset charset, final boolean append)269     private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
270         final boolean fileExistedAlready = file.exists();
271         try {
272             return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset));
273 
274         } catch (final IOException | RuntimeException ex) {
275             FileUtils.deleteQuietly(lockFile);
276             if (!fileExistedAlready) {
277                 FileUtils.deleteQuietly(file);
278             }
279             throw ex;
280         }
281     }
282 
283     /**
284      * Tests that we can write to the lock directory.
285      *
286      * @param lockDir  the File representing the lock directory
287      * @throws IOException if we cannot write to the lock directory
288      * @throws IOException if we cannot find the lock file
289      */
testLockDir(final File lockDir)290     private void testLockDir(final File lockDir) throws IOException {
291         if (!lockDir.exists()) {
292             throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath());
293         }
294         if (!lockDir.canWrite()) {
295             throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath());
296         }
297     }
298 
299     /**
300      * Writes the characters from an array.
301      * @param cbuf the characters to write
302      * @throws IOException if an I/O error occurs.
303      */
304     @Override
write(final char[] cbuf)305     public void write(final char[] cbuf) throws IOException {
306         out.write(cbuf);
307     }
308 
309     /**
310      * Writes the specified characters from an array.
311      * @param cbuf the characters to write
312      * @param off The start offset
313      * @param len The number of characters to write
314      * @throws IOException if an I/O error occurs.
315      */
316     @Override
write(final char[] cbuf, final int off, final int len)317     public void write(final char[] cbuf, final int off, final int len) throws IOException {
318         out.write(cbuf, off, len);
319     }
320 
321     /**
322      * Writes a character.
323      * @param c the character to write
324      * @throws IOException if an I/O error occurs.
325      */
326     @Override
write(final int c)327     public void write(final int c) throws IOException {
328         out.write(c);
329     }
330 
331     /**
332      * Writes the characters from a string.
333      * @param str the string to write
334      * @throws IOException if an I/O error occurs.
335      */
336     @Override
write(final String str)337     public void write(final String str) throws IOException {
338         out.write(str);
339     }
340 
341     /**
342      * Writes the specified characters from a string.
343      * @param str the string to write
344      * @param off The start offset
345      * @param len The number of characters to write
346      * @throws IOException if an I/O error occurs.
347      */
348     @Override
write(final String str, final int off, final int len)349     public void write(final String str, final int off, final int len) throws IOException {
350         out.write(str, off, len);
351     }
352 
353 }
354