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.UnsupportedEncodingException;
025import java.io.Writer;
026import java.nio.charset.Charset;
027import java.util.Objects;
028
029import org.apache.commons.io.Charsets;
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.io.build.AbstractOrigin;
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 * <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
041 * that the lock 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 = newFileOrigin(FileUtils.getTempDirectoryPath());
078
079        /**
080         * Constructs a new builder of {@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 an aspect that supports {@link File} on this builder, otherwise, this method throws an exception.
091         * </p>
092         * <p>
093         * This builder uses the following aspects:
094         * </p>
095         * <ul>
096         * <li>{@link File} is the target aspect.</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         * @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}