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