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