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.OutputStreamWriter;
23  import java.io.Writer;
24  import java.nio.charset.Charset;
25  
26  import org.apache.commons.io.Charsets;
27  import org.apache.commons.io.FileUtils;
28  
29  /**
30   * FileWriter that will create and honor lock files to allow simple
31   * cross thread file lock handling.
32   * <p>
33   * This class provides a simple alternative to <code>FileWriter</code>
34   * that will use a lock file to prevent duplicate writes.
35   * <p>
36   * <b>N.B.</b> the lock file is deleted when {@link #close()} is called
37   * - or if the main file cannot be opened initially.
38   * In the (unlikely) event that the lockfile cannot be deleted,
39   * this is not reported, and subsequent requests using
40   * the same lockfile will fail.
41   * <p>
42   * By default, the file will be overwritten, but this may be changed to append.
43   * The lock directory may be specified, but defaults to the system property
44   * <code>java.io.tmpdir</code>.
45   * The encoding may also be specified, and defaults to the platform default.
46   *
47   */
48  public class LockableFileWriter extends Writer {
49      // Cannot extend ProxyWriter, as requires writer to be
50      // known when super() is called
51  
52      /** The extension for the lock file. */
53      private static final String LCK = ".lck";
54  
55      /** The writer to decorate. */
56      private final Writer out;
57      /** The lock file. */
58      private final File lockFile;
59  
60      /**
61       * Constructs a LockableFileWriter.
62       * If the file exists, it is overwritten.
63       *
64       * @param fileName  the file to write to, not null
65       * @throws NullPointerException if the file is null
66       * @throws IOException in case of an I/O error
67       */
68      public LockableFileWriter(final String fileName) throws IOException {
69          this(fileName, false, null);
70      }
71  
72      /**
73       * Constructs a LockableFileWriter.
74       *
75       * @param fileName  file to write to, not null
76       * @param append  true if content should be appended, false to overwrite
77       * @throws NullPointerException if the file is null
78       * @throws IOException in case of an I/O error
79       */
80      public LockableFileWriter(final String fileName, final boolean append) throws IOException {
81          this(fileName, append, null);
82      }
83  
84      /**
85       * Constructs a LockableFileWriter.
86       *
87       * @param fileName  the file to write to, not null
88       * @param append  true if content should be appended, false to overwrite
89       * @param lockDir  the directory in which the lock file should be held
90       * @throws NullPointerException if the file is null
91       * @throws IOException in case of an I/O error
92       */
93      public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
94          this(new File(fileName), append, lockDir);
95      }
96  
97      /**
98       * Constructs a LockableFileWriter.
99       * 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 encoding  the encoding 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 encoding) throws IOException {
146         this(file, encoding, 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 encoding  the encoding to use, 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 encoding) throws IOException {
161         this(file, encoding, 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 encoding  the encoding to use, 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 encoding, 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, encoding, 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 encoding  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 encoding, final boolean append,
217             final String lockDir) throws IOException {
218         this(file, Charsets.toCharset(encoding), 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 encoding  the encoding 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 encoding, 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(encoding));
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 }