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