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 {@linkplain 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 }