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 * https://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 private File checkOriginFile() { 088 return checkOrigin().getFile(); 089 } 090 091 /** 092 * Constructs a new instance. 093 * <p> 094 * You must set an aspect that supports {@link File} on this builder, otherwise, this method throws an exception. 095 * </p> 096 * <p> 097 * This builder uses the following aspects: 098 * </p> 099 * <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}