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