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