001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io.output;
018
019import java.io.File;
020import java.io.FileOutputStream;
021import java.io.FileWriter;
022import java.io.IOException;
023import java.io.OutputStreamWriter;
024import java.io.Writer;
025import java.nio.charset.Charset;
026import java.util.Objects;
027
028import org.apache.commons.io.Charsets;
029import org.apache.commons.io.FileUtils;
030import org.apache.commons.io.build.AbstractOrigin;
031import org.apache.commons.io.build.AbstractOriginSupplier;
032import org.apache.commons.io.build.AbstractStreamBuilder;
033
034/**
035 * FileWriter that will create and honor lock files to allow simple cross thread file lock handling.
036 * <p>
037 * This class provides a simple alternative to {@link FileWriter} that will use a lock file to prevent duplicate writes.
038 * </p>
039 * <p>
040 * <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
041 * file cannot be deleted, an exception is thrown.
042 * </p>
043 * <p>
044 * 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
045 * {@code java.io.tmpdir}. The encoding may also be specified, and defaults to the platform default.
046 * </p>
047 * <p>
048 * To build an instance, use {@link Builder}.
049 * </p>
050 *
051 * @see Builder
052 */
053public class LockableFileWriter extends Writer {
054
055    // @formatter:off
056    /**
057     * Builds a new {@link LockableFileWriter}.
058     *
059     * <p>
060     * Using a CharsetEncoder:
061     * </p>
062     * <pre>{@code
063     * LockableFileWriter w = LockableFileWriter.builder()
064     *   .setPath(path)
065     *   .setAppend(false)
066     *   .setLockDirectory("Some/Directory")
067     *   .get();}
068     * </pre>
069     *
070     * @see #get()
071     * @since 2.12.0
072     */
073    // @formatter:on
074    public static class Builder extends AbstractStreamBuilder<LockableFileWriter, Builder> {
075
076        private boolean append;
077        private AbstractOrigin<?, ?> lockDirectory = AbstractOriginSupplier.newFileOrigin(FileUtils.getTempDirectoryPath());
078
079        /**
080         * Builds a new {@link LockableFileWriter}.
081         */
082        public Builder() {
083            setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
084            setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
085        }
086
087        /**
088         * Constructs a new instance.
089         * <p>
090         * You must set input that supports {@link File} on this builder, otherwise, this method throws an exception.
091         * </p>
092         * <p>
093         * This builder use the following aspects:
094         * </p>
095         * <ul>
096         * <li>{@link File}</li>
097         * <li>{@link #getCharset()}</li>
098         * <li>append</li>
099         * <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}