View Javadoc
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.IOException;
22  import java.io.OutputStream;
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  import org.apache.commons.io.IOUtils;
30  
31  /**
32   * FileWriter that will create and honor lock files to allow simple
33   * cross thread file lock handling.
34   * <p>
35   * This class provides a simple alternative to <code>FileWriter</code>
36   * that will use a lock file to prevent duplicate writes.
37   * <p>
38   * <b>N.B.</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 lockfile cannot be deleted,
41   * this is not reported, and subsequent requests using
42   * the same lockfile will fail.
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</code>.
47   * The encoding may also be specified, and defaults to the platform default.
48   *
49   * @version $Id: LockableFileWriter.java 1686747 2015-06-21 18:44:49Z krosenvold $
50   */
51  public class LockableFileWriter extends Writer {
52      // Cannot extend ProxyWriter, as requires writer to be
53      // known when super() is called
54  
55      /** The extension for the lock file. */
56      private static final String LCK = ".lck";
57  
58      /** The writer to decorate. */
59      private final Writer out;
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 fileName  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       */
71      public LockableFileWriter(final String fileName) throws IOException {
72          this(fileName, false, null);
73      }
74  
75      /**
76       * Constructs a LockableFileWriter.
77       *
78       * @param fileName  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       */
83      public LockableFileWriter(final String fileName, final boolean append) throws IOException {
84          this(fileName, append, null);
85      }
86  
87      /**
88       * Constructs a LockableFileWriter.
89       *
90       * @param fileName  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       */
96      public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
97          this(new File(fileName), append, lockDir);
98      }
99  
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 }