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.FileWriter;
22  import java.io.IOException;
23  import java.io.OutputStreamWriter;
24  import java.io.UnsupportedEncodingException;
25  import java.io.Writer;
26  import java.nio.charset.Charset;
27  import java.util.Objects;
28  
29  import org.apache.commons.io.Charsets;
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.build.AbstractOrigin;
32  import org.apache.commons.io.build.AbstractStreamBuilder;
33  
34  /**
35   * FileWriter that will create and honor lock files to allow simple cross thread file lock handling.
36   * <p>
37   * This class provides a simple alternative to {@link FileWriter} that will use a lock file to prevent duplicate writes.
38   * </p>
39   * <p>
40   * <strong>Note:</strong> The lock file is deleted when {@link #close()} is called - or if the main file cannot be opened initially. In the (unlikely) event
41   * that the lock file cannot be deleted, an exception is thrown.
42   * </p>
43   * <p>
44   * By default, the file will be overwritten, but this may be changed to append. The lock directory may be specified, but defaults to the system property
45   * {@code java.io.tmpdir}. The encoding may also be specified, and defaults to the platform default.
46   * </p>
47   * <p>
48   * To build an instance, use {@link Builder}.
49   * </p>
50   *
51   * @see Builder
52   */
53  public class LockableFileWriter extends Writer {
54  
55      // @formatter:off
56      /**
57       * Builds a new {@link LockableFileWriter}.
58       *
59       * <p>
60       * Using a CharsetEncoder:
61       * </p>
62       * <pre>{@code
63       * LockableFileWriter w = LockableFileWriter.builder()
64       *   .setPath(path)
65       *   .setAppend(false)
66       *   .setLockDirectory("Some/Directory")
67       *   .get();}
68       * </pre>
69       *
70       * @see #get()
71       * @since 2.12.0
72       */
73      // @formatter:on
74      public static class Builder extends AbstractStreamBuilder<LockableFileWriter, Builder> {
75  
76          private boolean append;
77          private AbstractOrigin<?, ?> lockDirectory = newFileOrigin(FileUtils.getTempDirectoryPath());
78  
79          /**
80           * Constructs a new builder of {@link LockableFileWriter}.
81           */
82          public Builder() {
83              setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
84              setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
85          }
86  
87          /**
88           * Constructs a new instance.
89           * <p>
90           * You must set an aspect that supports {@link File} on this builder, otherwise, this method throws an exception.
91           * </p>
92           * <p>
93           * This builder uses the following aspects:
94           * </p>
95           * <ul>
96           * <li>{@link File} is the target aspect.</li>
97           * <li>{@link #getCharset()}</li>
98           * <li>append</li>
99           * <li>lockDirectory</li>
100          * </ul>
101          *
102          * @return a new instance.
103          * @throws UnsupportedOperationException if the origin cannot provide a File.
104          * @throws IllegalStateException         if the {@code origin} is {@code null}.
105          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link #getFile()}.
106          * @see AbstractOrigin#getFile()
107          * @see #getUnchecked()
108          */
109         @Override
110         public LockableFileWriter get() throws IOException {
111             return new LockableFileWriter(checkOrigin().getFile(), getCharset(), append, lockDirectory.getFile().toString());
112         }
113 
114         /**
115          * Sets whether to append (true) or overwrite (false).
116          *
117          * @param append whether to append (true) or overwrite (false).
118          * @return {@code this} instance.
119          */
120         public Builder setAppend(final boolean append) {
121             this.append = append;
122             return this;
123         }
124 
125         /**
126          * Sets the directory in which the lock file should be held.
127          *
128          * @param lockDirectory the directory in which the lock file should be held.
129          * @return {@code this} instance.
130          */
131         public Builder setLockDirectory(final File lockDirectory) {
132             this.lockDirectory = newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectory());
133             return this;
134         }
135 
136         /**
137          * Sets the directory in which the lock file should be held.
138          *
139          * @param lockDirectory the directory in which the lock file should be held.
140          * @return {@code this} instance.
141          */
142         public Builder setLockDirectory(final String lockDirectory) {
143             this.lockDirectory = newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectoryPath());
144             return this;
145         }
146 
147     }
148 
149     /** The extension for the lock file. */
150     private static final String LCK = ".lck";
151 
152     // Cannot extend ProxyWriter, as requires writer to be
153     // known when super() is called
154 
155     /**
156      * Constructs a new {@link Builder}.
157      *
158      * @return a new {@link Builder}.
159      * @since 2.12.0
160      */
161     public static Builder builder() {
162         return new Builder();
163     }
164 
165     /** The writer to decorate. */
166     private final Writer out;
167 
168     /** The lock file. */
169     private final File lockFile;
170 
171     /**
172      * Constructs a LockableFileWriter. If the file exists, it is overwritten.
173      *
174      * @param file the file to write to, not null
175      * @throws NullPointerException if the file is null
176      * @throws IOException          in case of an I/O error
177      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
178      */
179     @Deprecated
180     public LockableFileWriter(final File file) throws IOException {
181         this(file, false, null);
182     }
183 
184     /**
185      * Constructs a LockableFileWriter.
186      *
187      * @param file   the file to write to, not null
188      * @param append true if content should be appended, false to overwrite
189      * @throws NullPointerException if the file is null
190      * @throws IOException          in case of an I/O error
191      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
192      */
193     @Deprecated
194     public LockableFileWriter(final File file, final boolean append) throws IOException {
195         this(file, append, null);
196     }
197 
198     /**
199      * Constructs a LockableFileWriter.
200      * <p>
201      * The new instance uses the virtual machine's {@link Charset#defaultCharset() default charset}.
202      * </p>
203      *
204      * @param file    the file to write to, not null
205      * @param append  true if content should be appended, false to overwrite
206      * @param lockDir the directory in which the lock file should be held
207      * @throws NullPointerException if the file is null
208      * @throws IOException          in case of an I/O error
209      * @deprecated Use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
210      */
211     @Deprecated
212     public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
213         this(file, Charset.defaultCharset(), append, lockDir);
214     }
215 
216     /**
217      * Constructs a LockableFileWriter with a file encoding.
218      *
219      * @param file    the file to write to, not null
220      * @param charset the charset to use, null means platform default
221      * @throws NullPointerException if the file is null
222      * @throws IOException          in case of an I/O error
223      * @since 2.3
224      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
225      */
226     @Deprecated
227     public LockableFileWriter(final File file, final Charset charset) throws IOException {
228         this(file, charset, false, null);
229     }
230 
231     /**
232      * Constructs a LockableFileWriter with a file encoding.
233      *
234      * @param file    the file to write to, not null
235      * @param charset the name of the requested charset, null means platform default
236      * @param append  true if content should be appended, false to overwrite
237      * @param lockDir the directory in which the lock file should be held
238      * @throws NullPointerException if the file is null
239      * @throws IOException          in case of an I/O error
240      * @since 2.3
241      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
242      */
243     @Deprecated
244     public LockableFileWriter(final File file, final Charset charset, final boolean append, final String lockDir) throws IOException {
245         // init file to create/append
246         final File absFile = Objects.requireNonNull(file, "file").getAbsoluteFile();
247         if (absFile.getParentFile() != null) {
248             FileUtils.forceMkdir(absFile.getParentFile());
249         }
250         if (absFile.isDirectory()) {
251             throw new IOException("File specified is a directory");
252         }
253 
254         // init lock file
255         final File lockDirFile = new File(lockDir != null ? lockDir : FileUtils.getTempDirectoryPath());
256         FileUtils.forceMkdir(lockDirFile);
257         testLockDir(lockDirFile);
258         lockFile = new File(lockDirFile, absFile.getName() + LCK);
259 
260         // check if locked
261         createLock();
262 
263         // init wrapped writer
264         out = initWriter(absFile, charset, append);
265     }
266 
267     /**
268      * Constructs a LockableFileWriter with a file encoding.
269      *
270      * @param file        the file to write to, not null
271      * @param charsetName the name of the requested charset, null means platform default
272      * @throws NullPointerException                         if the file is null
273      * @throws IOException                                  in case of an I/O error
274      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
275      *                                                      supported.
276      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
277      */
278     @Deprecated
279     public LockableFileWriter(final File file, final String charsetName) throws IOException {
280         this(file, charsetName, false, null);
281     }
282 
283     /**
284      * Constructs a LockableFileWriter with a file encoding.
285      *
286      * @param file        the file to write to, not null
287      * @param charsetName the encoding to use, null means platform default
288      * @param append      true if content should be appended, false to overwrite
289      * @param lockDir     the directory in which the lock file should be held
290      * @throws NullPointerException                         if the file is null
291      * @throws IOException                                  in case of an I/O error
292      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
293      *                                                      supported.
294      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
295      */
296     @Deprecated
297     public LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir) throws IOException {
298         this(file, Charsets.toCharset(charsetName), append, lockDir);
299     }
300 
301     /**
302      * Constructs a LockableFileWriter. If the file exists, it is overwritten.
303      *
304      * @param fileName the file to write to, not null
305      * @throws NullPointerException if the file is null
306      * @throws IOException          in case of an I/O error
307      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
308      */
309     @Deprecated
310     public LockableFileWriter(final String fileName) throws IOException {
311         this(fileName, false, null);
312     }
313 
314     /**
315      * Constructs a LockableFileWriter.
316      *
317      * @param fileName file to write to, not null
318      * @param append   true if content should be appended, false to overwrite
319      * @throws NullPointerException if the file is null
320      * @throws IOException          in case of an I/O error
321      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
322      */
323     @Deprecated
324     public LockableFileWriter(final String fileName, final boolean append) throws IOException {
325         this(fileName, append, null);
326     }
327 
328     /**
329      * Constructs a LockableFileWriter.
330      *
331      * @param fileName the file to write to, not null
332      * @param append   true if content should be appended, false to overwrite
333      * @param lockDir  the directory in which the lock file should be held
334      * @throws NullPointerException if the file is null
335      * @throws IOException          in case of an I/O error
336      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
337      */
338     @Deprecated
339     public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
340         this(new File(fileName), append, lockDir);
341     }
342 
343     /**
344      * Closes the file writer and deletes the lock file.
345      *
346      * @throws IOException if an I/O error occurs.
347      */
348     @Override
349     public void close() throws IOException {
350         try {
351             out.close();
352         } finally {
353             FileUtils.delete(lockFile);
354         }
355     }
356 
357     /**
358      * Creates the lock file.
359      *
360      * @throws IOException if we cannot create the file
361      */
362     private void createLock() throws IOException {
363         synchronized (LockableFileWriter.class) {
364             if (!lockFile.createNewFile()) {
365                 throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists");
366             }
367             lockFile.deleteOnExit();
368         }
369     }
370 
371     /**
372      * Flushes the stream.
373      *
374      * @throws IOException if an I/O error occurs.
375      */
376     @Override
377     public void flush() throws IOException {
378         out.flush();
379     }
380 
381     /**
382      * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
383      *
384      * @param file    the file to be accessed
385      * @param charset the charset to use
386      * @param append  true to append
387      * @return The initialized writer
388      * @throws IOException if an error occurs
389      */
390     private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
391         final boolean fileExistedAlready = file.exists();
392         try {
393             return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset));
394 
395         } catch (final IOException | RuntimeException ex) {
396             FileUtils.deleteQuietly(lockFile);
397             if (!fileExistedAlready) {
398                 FileUtils.deleteQuietly(file);
399             }
400             throw ex;
401         }
402     }
403 
404     /**
405      * Tests that we can write to the lock directory.
406      *
407      * @param lockDir the File representing the lock directory
408      * @throws IOException if we cannot write to the lock directory
409      * @throws IOException if we cannot find the lock file
410      */
411     private void testLockDir(final File lockDir) throws IOException {
412         if (!lockDir.exists()) {
413             throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath());
414         }
415         if (!lockDir.canWrite()) {
416             throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath());
417         }
418     }
419 
420     /**
421      * Writes the characters from an array.
422      *
423      * @param cbuf the characters to write
424      * @throws IOException if an I/O error occurs.
425      */
426     @Override
427     public void write(final char[] cbuf) throws IOException {
428         out.write(cbuf);
429     }
430 
431     /**
432      * Writes the specified characters from an array.
433      *
434      * @param cbuf the characters to write
435      * @param off  The start offset
436      * @param len  The number of characters to write
437      * @throws IOException if an I/O error occurs.
438      */
439     @Override
440     public void write(final char[] cbuf, final int off, final int len) throws IOException {
441         out.write(cbuf, off, len);
442     }
443 
444     /**
445      * Writes a character.
446      *
447      * @param c the character to write
448      * @throws IOException if an I/O error occurs.
449      */
450     @Override
451     public void write(final int c) throws IOException {
452         out.write(c);
453     }
454 
455     /**
456      * Writes the characters from a string.
457      *
458      * @param str the string to write
459      * @throws IOException if an I/O error occurs.
460      */
461     @Override
462     public void write(final String str) throws IOException {
463         out.write(str);
464     }
465 
466     /**
467      * Writes the specified characters from a string.
468      *
469      * @param str the string to write
470      * @param off The start offset
471      * @param len The number of characters to write
472      * @throws IOException if an I/O error occurs.
473      */
474     @Override
475     public void write(final String str, final int off, final int len) throws IOException {
476         out.write(str, off, len);
477     }
478 
479 }