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}