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 */
048public class LockableFileWriter extends Writer {
049
050    /**
051     * Builds a new {@link LockableFileWriter} instance.
052     * <p>
053     * Using a CharsetEncoder:
054     * </p>
055     * <pre>{@code
056     * LockableFileWriter w = LockableFileWriter.builder()
057     *   .setPath(path)
058     *   .setAppend(false)
059     *   .setLockDirectory("Some/Directory")
060     *   .get()}
061     * </pre>
062     * <p>
063     * @since 2.12.0
064     */
065    public static class Builder extends AbstractStreamBuilder<LockableFileWriter, Builder> {
066
067        private boolean append;
068        private AbstractOrigin<?, ?> lockDirectory = AbstractOriginSupplier.newFileOrigin(FileUtils.getTempDirectoryPath());
069
070        public Builder() {
071            setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
072            setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
073        }
074
075        /**
076         * Constructs a new instance.
077         *
078         * @throws UnsupportedOperationException if the origin cannot be converted to a File.
079         */
080        @Override
081        public LockableFileWriter get() throws IOException {
082            return new LockableFileWriter(getOrigin().getFile(), getCharset(), append, lockDirectory.getFile().toString());
083        }
084
085        /**
086         * Sets whether to append (true) or overwrite (false).
087         *
088         * @param append whether to append (true) or overwrite (false).
089         * @return this
090         */
091        public Builder setAppend(final boolean append) {
092            this.append = append;
093            return this;
094        }
095
096        /**
097         * Sets the directory in which the lock file should be held.
098         *
099         * @param lockDirectory the directory in which the lock file should be held.
100         * @return this
101         */
102        public Builder setLockDirectory(final File lockDirectory) {
103            this.lockDirectory = AbstractOriginSupplier.newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectory());
104            return this;
105        }
106
107        /**
108         * Sets the directory in which the lock file should be held.
109         *
110         * @param lockDirectory the directory in which the lock file should be held.
111         * @return this
112         */
113        public Builder setLockDirectory(final String lockDirectory) {
114            this.lockDirectory = AbstractOriginSupplier.newFileOrigin(lockDirectory != null ? lockDirectory : FileUtils.getTempDirectoryPath());
115            return this;
116        }
117
118    }
119
120    /** The extension for the lock file. */
121    private static final String LCK = ".lck";
122
123    // Cannot extend ProxyWriter, as requires writer to be
124    // known when super() is called
125
126    /**
127     * Constructs a new {@link Builder}.
128     *
129     * @return a new {@link Builder}.
130     * @since 2.12.0
131     */
132    public static Builder builder() {
133        return new Builder();
134    }
135
136    /** The writer to decorate. */
137    private final Writer out;
138
139    /** The lock file. */
140    private final File lockFile;
141
142    /**
143     * Constructs a LockableFileWriter. If the file exists, it is overwritten.
144     *
145     * @param file the file to write to, not null
146     * @throws NullPointerException if the file is null
147     * @throws IOException          in case of an I/O error
148     * @deprecated Use {@link #builder()}
149     */
150    @Deprecated
151    public LockableFileWriter(final File file) throws IOException {
152        this(file, false, null);
153    }
154
155    /**
156     * Constructs a LockableFileWriter.
157     *
158     * @param file   the file to write to, not null
159     * @param append true if content should be appended, false to overwrite
160     * @throws NullPointerException if the file is null
161     * @throws IOException          in case of an I/O error
162     * @deprecated Use {@link #builder()}
163     */
164    @Deprecated
165    public LockableFileWriter(final File file, final boolean append) throws IOException {
166        this(file, append, null);
167    }
168
169    /**
170     * Constructs a LockableFileWriter.
171     *
172     * @param file    the file to write to, not null
173     * @param append  true if content should be appended, false to overwrite
174     * @param lockDir the directory in which the lock file should be held
175     * @throws NullPointerException if the file is null
176     * @throws IOException          in case of an I/O error
177     * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
178     */
179    @Deprecated
180    public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
181        this(file, Charset.defaultCharset(), append, lockDir);
182    }
183
184    /**
185     * Constructs a LockableFileWriter with a file encoding.
186     *
187     * @param file    the file to write to, not null
188     * @param charset the charset to use, null means platform default
189     * @throws NullPointerException if the file is null
190     * @throws IOException          in case of an I/O error
191     * @since 2.3
192     * @deprecated Use {@link #builder()}
193     */
194    @Deprecated
195    public LockableFileWriter(final File file, final Charset charset) throws IOException {
196        this(file, charset, false, null);
197    }
198
199    /**
200     * Constructs a LockableFileWriter with a file encoding.
201     *
202     * @param file    the file to write to, not null
203     * @param charset the name of the requested charset, null means platform default
204     * @param append  true if content should be appended, false to overwrite
205     * @param lockDir the directory in which the lock file should be held
206     * @throws NullPointerException if the file is null
207     * @throws IOException          in case of an I/O error
208     * @since 2.3
209     * @deprecated Use {@link #builder()}
210     */
211    @Deprecated
212    public LockableFileWriter(final File file, final Charset charset, final boolean append, final String lockDir) throws IOException {
213        // init file to create/append
214        final File absFile = Objects.requireNonNull(file, "file").getAbsoluteFile();
215        if (absFile.getParentFile() != null) {
216            FileUtils.forceMkdir(absFile.getParentFile());
217        }
218        if (absFile.isDirectory()) {
219            throw new IOException("File specified is a directory");
220        }
221
222        // init lock file
223        final File lockDirFile = new File(lockDir != null ? lockDir : FileUtils.getTempDirectoryPath());
224        FileUtils.forceMkdir(lockDirFile);
225        testLockDir(lockDirFile);
226        lockFile = new File(lockDirFile, absFile.getName() + LCK);
227
228        // check if locked
229        createLock();
230
231        // init wrapped writer
232        out = initWriter(absFile, charset, append);
233    }
234
235    /**
236     * Constructs a LockableFileWriter with a file encoding.
237     *
238     * @param file        the file to write to, not null
239     * @param charsetName the name of the requested charset, null means platform default
240     * @throws NullPointerException                         if the file is null
241     * @throws IOException                                  in case of an I/O error
242     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
243     *                                                      supported.
244     * @deprecated Use {@link #builder()}
245     */
246    @Deprecated
247    public LockableFileWriter(final File file, final String charsetName) throws IOException {
248        this(file, charsetName, false, null);
249    }
250
251    /**
252     * Constructs a LockableFileWriter with a file encoding.
253     *
254     * @param file        the file to write to, not null
255     * @param charsetName the encoding to use, null means platform default
256     * @param append      true if content should be appended, false to overwrite
257     * @param lockDir     the directory in which the lock file should be held
258     * @throws NullPointerException                         if the file is null
259     * @throws IOException                                  in case of an I/O error
260     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not
261     *                                                      supported.
262     * @deprecated Use {@link #builder()}
263     */
264    @Deprecated
265    public LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir) throws IOException {
266        this(file, Charsets.toCharset(charsetName), append, lockDir);
267    }
268
269    /**
270     * Constructs a LockableFileWriter. If the file exists, it is overwritten.
271     *
272     * @param fileName the file to write to, not null
273     * @throws NullPointerException if the file is null
274     * @throws IOException          in case of an I/O error
275     * @deprecated Use {@link #builder()}
276     */
277    @Deprecated
278    public LockableFileWriter(final String fileName) throws IOException {
279        this(fileName, false, null);
280    }
281
282    /**
283     * Constructs a LockableFileWriter.
284     *
285     * @param fileName file to write to, not null
286     * @param append   true if content should be appended, false to overwrite
287     * @throws NullPointerException if the file is null
288     * @throws IOException          in case of an I/O error
289     * @deprecated Use {@link #builder()}
290     */
291    @Deprecated
292    public LockableFileWriter(final String fileName, final boolean append) throws IOException {
293        this(fileName, append, null);
294    }
295
296    /**
297     * Constructs a LockableFileWriter.
298     *
299     * @param fileName the file to write to, not null
300     * @param append   true if content should be appended, false to overwrite
301     * @param lockDir  the directory in which the lock file should be held
302     * @throws NullPointerException if the file is null
303     * @throws IOException          in case of an I/O error
304     * @deprecated Use {@link #builder()}
305     */
306    @Deprecated
307    public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
308        this(new File(fileName), append, lockDir);
309    }
310
311    /**
312     * Closes the file writer and deletes the lock file.
313     *
314     * @throws IOException if an I/O error occurs.
315     */
316    @Override
317    public void close() throws IOException {
318        try {
319            out.close();
320        } finally {
321            FileUtils.delete(lockFile);
322        }
323    }
324
325    /**
326     * Creates the lock file.
327     *
328     * @throws IOException if we cannot create the file
329     */
330    private void createLock() throws IOException {
331        synchronized (LockableFileWriter.class) {
332            if (!lockFile.createNewFile()) {
333                throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists");
334            }
335            lockFile.deleteOnExit();
336        }
337    }
338
339    /**
340     * Flushes the stream.
341     *
342     * @throws IOException if an I/O error occurs.
343     */
344    @Override
345    public void flush() throws IOException {
346        out.flush();
347    }
348
349    /**
350     * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails.
351     *
352     * @param file    the file to be accessed
353     * @param charset the charset to use
354     * @param append  true to append
355     * @return The initialized writer
356     * @throws IOException if an error occurs
357     */
358    private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException {
359        final boolean fileExistedAlready = file.exists();
360        try {
361            return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset));
362
363        } catch (final IOException | RuntimeException ex) {
364            FileUtils.deleteQuietly(lockFile);
365            if (!fileExistedAlready) {
366                FileUtils.deleteQuietly(file);
367            }
368            throw ex;
369        }
370    }
371
372    /**
373     * Tests that we can write to the lock directory.
374     *
375     * @param lockDir the File representing the lock directory
376     * @throws IOException if we cannot write to the lock directory
377     * @throws IOException if we cannot find the lock file
378     */
379    private void testLockDir(final File lockDir) throws IOException {
380        if (!lockDir.exists()) {
381            throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath());
382        }
383        if (!lockDir.canWrite()) {
384            throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath());
385        }
386    }
387
388    /**
389     * Writes the characters from an array.
390     *
391     * @param cbuf the characters to write
392     * @throws IOException if an I/O error occurs.
393     */
394    @Override
395    public void write(final char[] cbuf) throws IOException {
396        out.write(cbuf);
397    }
398
399    /**
400     * Writes the specified characters from an array.
401     *
402     * @param cbuf the characters to write
403     * @param off  The start offset
404     * @param len  The number of characters to write
405     * @throws IOException if an I/O error occurs.
406     */
407    @Override
408    public void write(final char[] cbuf, final int off, final int len) throws IOException {
409        out.write(cbuf, off, len);
410    }
411
412    /**
413     * Writes a character.
414     *
415     * @param c the character to write
416     * @throws IOException if an I/O error occurs.
417     */
418    @Override
419    public void write(final int c) throws IOException {
420        out.write(c);
421    }
422
423    /**
424     * Writes the characters from a string.
425     *
426     * @param str the string to write
427     * @throws IOException if an I/O error occurs.
428     */
429    @Override
430    public void write(final String str) throws IOException {
431        out.write(str);
432    }
433
434    /**
435     * Writes the specified characters from a string.
436     *
437     * @param str the string to write
438     * @param off The start offset
439     * @param len The number of characters to write
440     * @throws IOException if an I/O error occurs.
441     */
442    @Override
443    public void write(final String str, final int off, final int len) throws IOException {
444        out.write(str, off, len);
445    }
446
447}