1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.io.output; 18 19 import java.io.File; 20 import java.io.FileOutputStream; 21 import java.io.FileWriter; 22 import java.io.IOException; 23 import java.io.OutputStreamWriter; 24 import java.io.Writer; 25 import java.nio.charset.Charset; 26 27 import org.apache.commons.io.Charsets; 28 import org.apache.commons.io.FileUtils; 29 30 /** 31 * FileWriter that will create and honor lock files to allow simple 32 * cross thread file lock handling. 33 * <p> 34 * This class provides a simple alternative to {@link FileWriter} 35 * that will use a lock file to prevent duplicate writes. 36 * </p> 37 * <p> 38 * <b>Note:</b> The lock file is deleted when {@link #close()} is called 39 * - or if the main file cannot be opened initially. 40 * In the (unlikely) event that the lock file cannot be deleted, 41 * an exception is thrown. 42 * </p> 43 * <p> 44 * By default, the file will be overwritten, but this may be changed to append. 45 * The lock directory may be specified, but defaults to the system property 46 * {@code java.io.tmpdir}. 47 * The encoding may also be specified, and defaults to the platform default. 48 * </p> 49 */ 50 public class LockableFileWriter extends Writer { 51 // Cannot extend ProxyWriter, as requires writer to be 52 // known when super() is called 53 54 /** The extension for the lock file. */ 55 private static final String LCK = ".lck"; 56 57 /** The writer to decorate. */ 58 private final Writer out; 59 60 /** The lock file. */ 61 private final File lockFile; 62 63 /** 64 * Constructs a LockableFileWriter. 65 * If the file exists, it is overwritten. 66 * 67 * @param file the file to write to, not null 68 * @throws NullPointerException if the file is null 69 * @throws IOException in case of an I/O error 70 */ LockableFileWriter(final File file)71 public LockableFileWriter(final File file) throws IOException { 72 this(file, false, null); 73 } 74 75 /** 76 * Constructs a LockableFileWriter. 77 * 78 * @param file the file to write to, not null 79 * @param append true if content should be appended, false to overwrite 80 * @throws NullPointerException if the file is null 81 * @throws IOException in case of an I/O error 82 */ LockableFileWriter(final File file, final boolean append)83 public LockableFileWriter(final File file, final boolean append) throws IOException { 84 this(file, append, null); 85 } 86 87 /** 88 * Constructs a LockableFileWriter. 89 * 90 * @param file the file to write to, not null 91 * @param append true if content should be appended, false to overwrite 92 * @param lockDir the directory in which the lock file should be held 93 * @throws NullPointerException if the file is null 94 * @throws IOException in case of an I/O error 95 * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead 96 */ 97 @Deprecated LockableFileWriter(final File file, final boolean append, final String lockDir)98 public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException { 99 this(file, Charset.defaultCharset(), append, lockDir); 100 } 101 102 /** 103 * Constructs a LockableFileWriter with a file encoding. 104 * 105 * @param file the file to write to, not null 106 * @param charset the charset to use, null means platform default 107 * @throws NullPointerException if the file is null 108 * @throws IOException in case of an I/O error 109 * @since 2.3 110 */ LockableFileWriter(final File file, final Charset charset)111 public LockableFileWriter(final File file, final Charset charset) throws IOException { 112 this(file, charset, false, null); 113 } 114 115 /** 116 * Constructs a LockableFileWriter with a file encoding. 117 * 118 * @param file the file to write to, not null 119 * @param charset the name of the requested charset, null means platform default 120 * @param append true if content should be appended, false to overwrite 121 * @param lockDir the directory in which the lock file should be held 122 * @throws NullPointerException if the file is null 123 * @throws IOException in case of an I/O error 124 * @since 2.3 125 */ LockableFileWriter(final File file, final Charset charset, final boolean append, String lockDir)126 public LockableFileWriter(final File file, final Charset charset, final boolean append, String lockDir) throws IOException { 127 // init file to create/append 128 final File absFile = file.getAbsoluteFile(); 129 if (absFile.getParentFile() != null) { 130 FileUtils.forceMkdir(absFile.getParentFile()); 131 } 132 if (absFile.isDirectory()) { 133 throw new IOException("File specified is a directory"); 134 } 135 136 // init lock file 137 if (lockDir == null) { 138 lockDir = FileUtils.getTempDirectoryPath(); 139 } 140 final File lockDirFile = new File(lockDir); 141 FileUtils.forceMkdir(lockDirFile); 142 testLockDir(lockDirFile); 143 lockFile = new File(lockDirFile, absFile.getName() + LCK); 144 145 // check if locked 146 createLock(); 147 148 // init wrapped writer 149 out = initWriter(absFile, charset, append); 150 } 151 152 /** 153 * Constructs a LockableFileWriter with a file encoding. 154 * 155 * @param file the file to write to, not null 156 * @param charsetName the name of the requested charset, null means platform default 157 * @throws NullPointerException if the file is null 158 * @throws IOException in case of an I/O error 159 * @throws java.nio.charset.UnsupportedCharsetException 160 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 161 * supported. 162 */ LockableFileWriter(final File file, final String charsetName)163 public LockableFileWriter(final File file, final String charsetName) throws IOException { 164 this(file, charsetName, false, null); 165 } 166 167 /** 168 * Constructs a LockableFileWriter with a file encoding. 169 * 170 * @param file the file to write to, not null 171 * @param charsetName the encoding to use, null means platform default 172 * @param append true if content should be appended, false to overwrite 173 * @param lockDir the directory in which the lock file should be held 174 * @throws NullPointerException if the file is null 175 * @throws IOException in case of an I/O error 176 * @throws java.nio.charset.UnsupportedCharsetException 177 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 178 * supported. 179 */ LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir)180 public LockableFileWriter(final File file, final String charsetName, final boolean append, 181 final String lockDir) throws IOException { 182 this(file, Charsets.toCharset(charsetName), append, lockDir); 183 } 184 185 /** 186 * Constructs a LockableFileWriter. 187 * If the file exists, it is overwritten. 188 * 189 * @param fileName the file to write to, not null 190 * @throws NullPointerException if the file is null 191 * @throws IOException in case of an I/O error 192 */ LockableFileWriter(final String fileName)193 public LockableFileWriter(final String fileName) throws IOException { 194 this(fileName, false, null); 195 } 196 197 /** 198 * Constructs a LockableFileWriter. 199 * 200 * @param fileName file to write to, not null 201 * @param append true if content should be appended, false to overwrite 202 * @throws NullPointerException if the file is null 203 * @throws IOException in case of an I/O error 204 */ LockableFileWriter(final String fileName, final boolean append)205 public LockableFileWriter(final String fileName, final boolean append) throws IOException { 206 this(fileName, append, null); 207 } 208 209 /** 210 * Constructs a LockableFileWriter. 211 * 212 * @param fileName the file to write to, not null 213 * @param append true if content should be appended, false to overwrite 214 * @param lockDir the directory in which the lock file should be held 215 * @throws NullPointerException if the file is null 216 * @throws IOException in case of an I/O error 217 */ LockableFileWriter(final String fileName, final boolean append, final String lockDir)218 public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException { 219 this(new File(fileName), append, lockDir); 220 } 221 222 /** 223 * Closes the file writer and deletes the lock file. 224 * 225 * @throws IOException if an I/O error occurs. 226 */ 227 @Override close()228 public void close() throws IOException { 229 try { 230 out.close(); 231 } finally { 232 FileUtils.delete(lockFile); 233 } 234 } 235 236 /** 237 * Creates the lock file. 238 * 239 * @throws IOException if we cannot create the file 240 */ createLock()241 private void createLock() throws IOException { 242 synchronized (LockableFileWriter.class) { 243 if (!lockFile.createNewFile()) { 244 throw new IOException("Can't write file, lock " + lockFile.getAbsolutePath() + " exists"); 245 } 246 lockFile.deleteOnExit(); 247 } 248 } 249 250 /** 251 * Flushes the stream. 252 * @throws IOException if an I/O error occurs. 253 */ 254 @Override flush()255 public void flush() throws IOException { 256 out.flush(); 257 } 258 259 /** 260 * Initializes the wrapped file writer. 261 * Ensure that a cleanup occurs if the writer creation fails. 262 * 263 * @param file the file to be accessed 264 * @param charset the charset to use 265 * @param append true to append 266 * @return The initialized writer 267 * @throws IOException if an error occurs 268 */ initWriter(final File file, final Charset charset, final boolean append)269 private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException { 270 final boolean fileExistedAlready = file.exists(); 271 try { 272 return new OutputStreamWriter(new FileOutputStream(file.getAbsolutePath(), append), Charsets.toCharset(charset)); 273 274 } catch (final IOException | RuntimeException ex) { 275 FileUtils.deleteQuietly(lockFile); 276 if (!fileExistedAlready) { 277 FileUtils.deleteQuietly(file); 278 } 279 throw ex; 280 } 281 } 282 283 /** 284 * Tests that we can write to the lock directory. 285 * 286 * @param lockDir the File representing the lock directory 287 * @throws IOException if we cannot write to the lock directory 288 * @throws IOException if we cannot find the lock file 289 */ testLockDir(final File lockDir)290 private void testLockDir(final File lockDir) throws IOException { 291 if (!lockDir.exists()) { 292 throw new IOException("Could not find lockDir: " + lockDir.getAbsolutePath()); 293 } 294 if (!lockDir.canWrite()) { 295 throw new IOException("Could not write to lockDir: " + lockDir.getAbsolutePath()); 296 } 297 } 298 299 /** 300 * Writes the characters from an array. 301 * @param cbuf the characters to write 302 * @throws IOException if an I/O error occurs. 303 */ 304 @Override write(final char[] cbuf)305 public void write(final char[] cbuf) throws IOException { 306 out.write(cbuf); 307 } 308 309 /** 310 * Writes the specified characters from an array. 311 * @param cbuf the characters to write 312 * @param off The start offset 313 * @param len The number of characters to write 314 * @throws IOException if an I/O error occurs. 315 */ 316 @Override write(final char[] cbuf, final int off, final int len)317 public void write(final char[] cbuf, final int off, final int len) throws IOException { 318 out.write(cbuf, off, len); 319 } 320 321 /** 322 * Writes a character. 323 * @param c the character to write 324 * @throws IOException if an I/O error occurs. 325 */ 326 @Override write(final int c)327 public void write(final int c) throws IOException { 328 out.write(c); 329 } 330 331 /** 332 * Writes the characters from a string. 333 * @param str the string to write 334 * @throws IOException if an I/O error occurs. 335 */ 336 @Override write(final String str)337 public void write(final String str) throws IOException { 338 out.write(str); 339 } 340 341 /** 342 * Writes the specified characters from a string. 343 * @param str the string to write 344 * @param off The start offset 345 * @param len The number of characters to write 346 * @throws IOException if an I/O error occurs. 347 */ 348 @Override write(final String str, final int off, final int len)349 public void write(final String str, final int off, final int len) throws IOException { 350 out.write(str, off, len); 351 } 352 353 } 354