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