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.UnsupportedEncodingException;
25  import java.io.Writer;
26  import java.nio.charset.Charset;
27  import java.nio.charset.UnsupportedCharsetException;
28  
29  import org.apache.commons.io.Charsets;
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.IOUtils;
32  
33  /**
34   * FileWriter that will create and honor lock files to allow simple
35   * cross thread file lock handling.
36   * <p>
37   * This class provides a simple alternative to <code>FileWriter</code>
38   * that will use a lock file to prevent duplicate writes.
39   * <p>
40   * <b>N.B.</b> the lock file is deleted when {@link #close()} is called
41   * - or if the main file cannot be opened initially.
42   * In the (unlikely) event that the lockfile cannot be deleted,
43   * this is not reported, and subsequent requests using
44   * the same lockfile will fail.
45   * <p>
46   * By default, the file will be overwritten, but this may be changed to append.
47   * The lock directory may be specified, but defaults to the system property
48   * <code>java.io.tmpdir</code>.
49   * The encoding may also be specified, and defaults to the platform default.
50   *
51   * @version $Id: LockableFileWriter.java 1471767 2013-04-24 23:24:19Z sebb $
52   */
53  public class LockableFileWriter extends Writer {
54      // Cannot extend ProxyWriter, as requires writer to be
55      // known when super() is called
56  
57      /** The extension for the lock file. */
58      private static final String LCK = ".lck";
59  
60      /** The writer to decorate. */
61      private final Writer out;
62      /** The lock file. */
63      private final File lockFile;
64  
65      /**
66       * Constructs a LockableFileWriter.
67       * If the file exists, it is overwritten.
68       *
69       * @param fileName  the file to write to, not null
70       * @throws NullPointerException if the file is null
71       * @throws IOException in case of an I/O error
72       */
73      public LockableFileWriter(final String fileName) throws IOException {
74          this(fileName, false, null);
75      }
76  
77      /**
78       * Constructs a LockableFileWriter.
79       *
80       * @param fileName  file to write to, not null
81       * @param append  true if content should be appended, false to overwrite
82       * @throws NullPointerException if the file is null
83       * @throws IOException in case of an I/O error
84       */
85      public LockableFileWriter(final String fileName, final boolean append) throws IOException {
86          this(fileName, append, null);
87      }
88  
89      /**
90       * Constructs a LockableFileWriter.
91       *
92       * @param fileName  the file to write to, not null
93       * @param append  true if content should be appended, false to overwrite
94       * @param lockDir  the directory in which the lock file should be held
95       * @throws NullPointerException if the file is null
96       * @throws IOException in case of an I/O error
97       */
98      public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
99          this(new File(fileName), append, lockDir);
100     }
101 
102     /**
103      * Constructs a LockableFileWriter.
104      * If the file exists, it is overwritten.
105      *
106      * @param file  the file to write to, not null
107      * @throws NullPointerException if the file is null
108      * @throws IOException in case of an I/O error
109      */
110     public LockableFileWriter(final File file) throws IOException {
111         this(file, false, null);
112     }
113 
114     /**
115      * Constructs a LockableFileWriter.
116      *
117      * @param file  the file to write to, not null
118      * @param append  true if content should be appended, false to overwrite
119      * @throws NullPointerException if the file is null
120      * @throws IOException in case of an I/O error
121      */
122     public LockableFileWriter(final File file, final boolean append) throws IOException {
123         this(file, append, null);
124     }
125 
126     /**
127      * Constructs a LockableFileWriter.
128      *
129      * @param file  the file to write to, not null
130      * @param append  true if content should be appended, false to overwrite
131      * @param lockDir  the directory in which the lock file should be held
132      * @throws NullPointerException if the file is null
133      * @throws IOException in case of an I/O error
134      * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
135      */
136     @Deprecated
137     public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
138         this(file, Charset.defaultCharset(), append, lockDir);
139     }
140 
141     /**
142      * Constructs a LockableFileWriter with a file encoding.
143      *
144      * @param file  the file to write to, not null
145      * @param encoding  the encoding to use, null means platform default
146      * @throws NullPointerException if the file is null
147      * @throws IOException in case of an I/O error
148      * @since 2.3
149      */
150     public LockableFileWriter(final File file, final Charset encoding) throws IOException {
151         this(file, encoding, false, null);
152     }
153 
154     /**
155      * Constructs a LockableFileWriter with a file encoding.
156      *
157      * @param file  the file to write to, not null
158      * @param encoding  the encoding to use, null means platform default
159      * @throws NullPointerException if the file is null
160      * @throws IOException in case of an I/O error
161      * @throws UnsupportedCharsetException
162      *             thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
163      *             supported.
164      */
165     public LockableFileWriter(final File file, final String encoding) throws IOException {
166         this(file, encoding, false, null);
167     }
168 
169     /**
170      * Constructs a LockableFileWriter with a file encoding.
171      *
172      * @param file  the file to write to, not null
173      * @param encoding  the encoding to use, null means platform default
174      * @param append  true if content should be appended, false to overwrite
175      * @param lockDir  the directory in which the lock file should be held
176      * @throws NullPointerException if the file is null
177      * @throws IOException in case of an I/O error
178      * @since 2.3
179      */
180     public LockableFileWriter(File file, final Charset encoding, final boolean append,
181             String lockDir) throws IOException {
182         super();
183         // init file to create/append
184         file = file.getAbsoluteFile();
185         if (file.getParentFile() != null) {
186             FileUtils.forceMkdir(file.getParentFile());
187         }
188         if (file.isDirectory()) {
189             throw new IOException("File specified is a directory");
190         }
191 
192         // init lock file
193         if (lockDir == null) {
194             lockDir = System.getProperty("java.io.tmpdir");
195         }
196         final File lockDirFile = new File(lockDir);
197         FileUtils.forceMkdir(lockDirFile);
198         testLockDir(lockDirFile);
199         lockFile = new File(lockDirFile, file.getName() + LCK);
200 
201         // check if locked
202         createLock();
203 
204         // init wrapped writer
205         out = initWriter(file, encoding, append);
206     }
207 
208     /**
209      * Constructs a LockableFileWriter with a file encoding.
210      *
211      * @param file  the file to write to, not null
212      * @param encoding  the encoding to use, null means platform default
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      * @throws UnsupportedCharsetException
218      *             thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
219      *             supported.
220      */
221     public LockableFileWriter(final File file, final String encoding, final boolean append,
222             final String lockDir) throws IOException {
223         this(file, Charsets.toCharset(encoding), append, lockDir);
224     }
225 
226     //-----------------------------------------------------------------------
227     /**
228      * Tests that we can write to the lock directory.
229      *
230      * @param lockDir  the File representing the lock directory
231      * @throws IOException if we cannot write to the lock directory
232      * @throws IOException if we cannot find the lock file
233      */
234     private void testLockDir(final File lockDir) throws IOException {
235         if (!lockDir.exists()) {
236             throw new IOException(
237                     "Could not find lockDir: " + lockDir.getAbsolutePath());
238         }
239         if (!lockDir.canWrite()) {
240             throw new IOException(
241                     "Could not write to lockDir: " + lockDir.getAbsolutePath());
242         }
243     }
244 
245     /**
246      * Creates the lock file.
247      *
248      * @throws IOException if we cannot create the file
249      */
250     private void createLock() throws IOException {
251         synchronized (LockableFileWriter.class) {
252             if (!lockFile.createNewFile()) {
253                 throw new IOException("Can't write file, lock " +
254                         lockFile.getAbsolutePath() + " exists");
255             }
256             lockFile.deleteOnExit();
257         }
258     }
259 
260     /**
261      * Initialise the wrapped file writer.
262      * Ensure that a cleanup occurs if the writer creation fails.
263      *
264      * @param file  the file to be accessed
265      * @param encoding  the encoding to use
266      * @param append  true to append
267      * @return The initialised writer
268      * @throws IOException if an error occurs
269      */
270     private Writer initWriter(final File file, final Charset encoding, final boolean append) throws IOException {
271         final boolean fileExistedAlready = file.exists();
272         OutputStream stream = null;
273         Writer writer = null;
274         try {
275             stream = new FileOutputStream(file.getAbsolutePath(), append);
276             writer = new OutputStreamWriter(stream, Charsets.toCharset(encoding));
277         } catch (final IOException ex) {
278             IOUtils.closeQuietly(writer);
279             IOUtils.closeQuietly(stream);
280             FileUtils.deleteQuietly(lockFile);
281             if (fileExistedAlready == false) {
282                 FileUtils.deleteQuietly(file);
283             }
284             throw ex;
285         } catch (final RuntimeException ex) {
286             IOUtils.closeQuietly(writer);
287             IOUtils.closeQuietly(stream);
288             FileUtils.deleteQuietly(lockFile);
289             if (fileExistedAlready == false) {
290                 FileUtils.deleteQuietly(file);
291             }
292             throw ex;
293         }
294         return writer;
295     }
296 
297     //-----------------------------------------------------------------------
298     /**
299      * Closes the file writer and deletes the lockfile (if possible).
300      *
301      * @throws IOException if an I/O error occurs
302      */
303     @Override
304     public void close() throws IOException {
305         try {
306             out.close();
307         } finally {
308             lockFile.delete();
309         }
310     }
311 
312     //-----------------------------------------------------------------------
313     /**
314      * Write a character.
315      * @param idx the character to write
316      * @throws IOException if an I/O error occurs
317      */
318     @Override
319     public void write(final int idx) throws IOException {
320         out.write(idx);
321     }
322 
323     /**
324      * Write the characters from an array.
325      * @param chr the characters to write
326      * @throws IOException if an I/O error occurs
327      */
328     @Override
329     public void write(final char[] chr) throws IOException {
330         out.write(chr);
331     }
332 
333     /**
334      * Write the specified characters from an array.
335      * @param chr the characters to write
336      * @param st The start offset
337      * @param end The number of characters to write
338      * @throws IOException if an I/O error occurs
339      */
340     @Override
341     public void write(final char[] chr, final int st, final int end) throws IOException {
342         out.write(chr, st, end);
343     }
344 
345     /**
346      * Write the characters from a string.
347      * @param str the string to write
348      * @throws IOException if an I/O error occurs
349      */
350     @Override
351     public void write(final String str) throws IOException {
352         out.write(str);
353     }
354 
355     /**
356      * Write the specified characters from a string.
357      * @param str the string to write
358      * @param st The start offset
359      * @param end The number of characters to write
360      * @throws IOException if an I/O error occurs
361      */
362     @Override
363     public void write(final String str, final int st, final int end) throws IOException {
364         out.write(str, st, end);
365     }
366 
367     /**
368      * Flush the stream.
369      * @throws IOException if an I/O error occurs
370      */
371     @Override
372     public void flush() throws IOException {
373         out.flush();
374     }
375 
376 }