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.IOException; 022import java.io.OutputStream; 023import java.io.OutputStreamWriter; 024import java.io.UnsupportedEncodingException; 025import java.io.Writer; 026import java.nio.charset.Charset; 027import java.nio.charset.UnsupportedCharsetException; 028 029import org.apache.commons.io.Charsets; 030import org.apache.commons.io.FileUtils; 031import org.apache.commons.io.IOUtils; 032 033/** 034 * FileWriter that will create and honor lock files to allow simple 035 * cross thread file lock handling. 036 * <p> 037 * This class provides a simple alternative to <code>FileWriter</code> 038 * that will use a lock file to prevent duplicate writes. 039 * <p> 040 * <b>N.B.</b> the lock file is deleted when {@link #close()} is called 041 * - or if the main file cannot be opened initially. 042 * In the (unlikely) event that the lockfile cannot be deleted, 043 * this is not reported, and subsequent requests using 044 * the same lockfile will fail. 045 * <p> 046 * By default, the file will be overwritten, but this may be changed to append. 047 * The lock directory may be specified, but defaults to the system property 048 * <code>java.io.tmpdir</code>. 049 * The encoding may also be specified, and defaults to the platform default. 050 * 051 * @version $Id: LockableFileWriter.java 1471767 2013-04-24 23:24:19Z sebb $ 052 */ 053public class LockableFileWriter extends Writer { 054 // Cannot extend ProxyWriter, as requires writer to be 055 // known when super() is called 056 057 /** The extension for the lock file. */ 058 private static final String LCK = ".lck"; 059 060 /** The writer to decorate. */ 061 private final Writer out; 062 /** The lock file. */ 063 private final File lockFile; 064 065 /** 066 * Constructs a LockableFileWriter. 067 * If the file exists, it is overwritten. 068 * 069 * @param fileName the file to write to, not null 070 * @throws NullPointerException if the file is null 071 * @throws IOException in case of an I/O error 072 */ 073 public LockableFileWriter(final String fileName) throws IOException { 074 this(fileName, false, null); 075 } 076 077 /** 078 * Constructs a LockableFileWriter. 079 * 080 * @param fileName file to write to, not null 081 * @param append true if content should be appended, false to overwrite 082 * @throws NullPointerException if the file is null 083 * @throws IOException in case of an I/O error 084 */ 085 public LockableFileWriter(final String fileName, final boolean append) throws IOException { 086 this(fileName, append, null); 087 } 088 089 /** 090 * Constructs a LockableFileWriter. 091 * 092 * @param fileName the file to write to, not null 093 * @param append true if content should be appended, false to overwrite 094 * @param lockDir the directory in which the lock file should be held 095 * @throws NullPointerException if the file is null 096 * @throws IOException in case of an I/O error 097 */ 098 public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException { 099 this(new File(fileName), append, lockDir); 100 } 101 102 /** 103 * Constructs a LockableFileWriter. 104 * If the file exists, it is overwritten. 105 * 106 * @param file the file to write to, not null 107 * @throws NullPointerException if the file is null 108 * @throws IOException in case of an I/O error 109 */ 110 public LockableFileWriter(final File file) throws IOException { 111 this(file, false, null); 112 } 113 114 /** 115 * Constructs a LockableFileWriter. 116 * 117 * @param file the file to write to, not null 118 * @param append true if content should be appended, false to overwrite 119 * @throws NullPointerException if the file is null 120 * @throws IOException in case of an I/O error 121 */ 122 public LockableFileWriter(final File file, final boolean append) throws IOException { 123 this(file, append, null); 124 } 125 126 /** 127 * Constructs a LockableFileWriter. 128 * 129 * @param file the file to write to, not null 130 * @param append true if content should be appended, false to overwrite 131 * @param lockDir the directory in which the lock file should be held 132 * @throws NullPointerException if the file is null 133 * @throws IOException in case of an I/O error 134 * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead 135 */ 136 @Deprecated 137 public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException { 138 this(file, Charset.defaultCharset(), append, lockDir); 139 } 140 141 /** 142 * Constructs a LockableFileWriter with a file encoding. 143 * 144 * @param file the file to write to, not null 145 * @param encoding the encoding to use, null means platform default 146 * @throws NullPointerException if the file is null 147 * @throws IOException in case of an I/O error 148 * @since 2.3 149 */ 150 public LockableFileWriter(final File file, final Charset encoding) throws IOException { 151 this(file, encoding, false, null); 152 } 153 154 /** 155 * Constructs a LockableFileWriter with a file encoding. 156 * 157 * @param file the file to write to, not null 158 * @param encoding the encoding to use, null means platform default 159 * @throws NullPointerException if the file is null 160 * @throws IOException in case of an I/O error 161 * @throws UnsupportedCharsetException 162 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not 163 * supported. 164 */ 165 public LockableFileWriter(final File file, final String encoding) throws IOException { 166 this(file, encoding, false, null); 167 } 168 169 /** 170 * Constructs a LockableFileWriter with a file encoding. 171 * 172 * @param file the file to write to, not null 173 * @param encoding the encoding to use, null means platform default 174 * @param append true if content should be appended, false to overwrite 175 * @param lockDir the directory in which the lock file should be held 176 * @throws NullPointerException if the file is null 177 * @throws IOException in case of an I/O error 178 * @since 2.3 179 */ 180 public LockableFileWriter(File file, final Charset encoding, final boolean append, 181 String lockDir) throws IOException { 182 super(); 183 // init file to create/append 184 file = file.getAbsoluteFile(); 185 if (file.getParentFile() != null) { 186 FileUtils.forceMkdir(file.getParentFile()); 187 } 188 if (file.isDirectory()) { 189 throw new IOException("File specified is a directory"); 190 } 191 192 // init lock file 193 if (lockDir == null) { 194 lockDir = System.getProperty("java.io.tmpdir"); 195 } 196 final File lockDirFile = new File(lockDir); 197 FileUtils.forceMkdir(lockDirFile); 198 testLockDir(lockDirFile); 199 lockFile = new File(lockDirFile, file.getName() + LCK); 200 201 // check if locked 202 createLock(); 203 204 // init wrapped writer 205 out = initWriter(file, encoding, append); 206 } 207 208 /** 209 * Constructs a LockableFileWriter with a file encoding. 210 * 211 * @param file the file to write to, not null 212 * @param encoding the encoding to use, null means platform default 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 * @throws UnsupportedCharsetException 218 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not 219 * supported. 220 */ 221 public LockableFileWriter(final File file, final String encoding, final boolean append, 222 final String lockDir) throws IOException { 223 this(file, Charsets.toCharset(encoding), append, lockDir); 224 } 225 226 //----------------------------------------------------------------------- 227 /** 228 * Tests that we can write to the lock directory. 229 * 230 * @param lockDir the File representing the lock directory 231 * @throws IOException if we cannot write to the lock directory 232 * @throws IOException if we cannot find the lock file 233 */ 234 private void testLockDir(final File lockDir) throws IOException { 235 if (!lockDir.exists()) { 236 throw new IOException( 237 "Could not find lockDir: " + lockDir.getAbsolutePath()); 238 } 239 if (!lockDir.canWrite()) { 240 throw new IOException( 241 "Could not write to lockDir: " + lockDir.getAbsolutePath()); 242 } 243 } 244 245 /** 246 * Creates the lock file. 247 * 248 * @throws IOException if we cannot create the file 249 */ 250 private void createLock() throws IOException { 251 synchronized (LockableFileWriter.class) { 252 if (!lockFile.createNewFile()) { 253 throw new IOException("Can't write file, lock " + 254 lockFile.getAbsolutePath() + " exists"); 255 } 256 lockFile.deleteOnExit(); 257 } 258 } 259 260 /** 261 * Initialise the wrapped file writer. 262 * Ensure that a cleanup occurs if the writer creation fails. 263 * 264 * @param file the file to be accessed 265 * @param encoding the encoding to use 266 * @param append true to append 267 * @return The initialised writer 268 * @throws IOException if an error occurs 269 */ 270 private Writer initWriter(final File file, final Charset encoding, final boolean append) throws IOException { 271 final boolean fileExistedAlready = file.exists(); 272 OutputStream stream = null; 273 Writer writer = null; 274 try { 275 stream = new FileOutputStream(file.getAbsolutePath(), append); 276 writer = new OutputStreamWriter(stream, Charsets.toCharset(encoding)); 277 } catch (final IOException ex) { 278 IOUtils.closeQuietly(writer); 279 IOUtils.closeQuietly(stream); 280 FileUtils.deleteQuietly(lockFile); 281 if (fileExistedAlready == false) { 282 FileUtils.deleteQuietly(file); 283 } 284 throw ex; 285 } catch (final RuntimeException ex) { 286 IOUtils.closeQuietly(writer); 287 IOUtils.closeQuietly(stream); 288 FileUtils.deleteQuietly(lockFile); 289 if (fileExistedAlready == false) { 290 FileUtils.deleteQuietly(file); 291 } 292 throw ex; 293 } 294 return writer; 295 } 296 297 //----------------------------------------------------------------------- 298 /** 299 * Closes the file writer and deletes the lockfile (if possible). 300 * 301 * @throws IOException if an I/O error occurs 302 */ 303 @Override 304 public void close() throws IOException { 305 try { 306 out.close(); 307 } finally { 308 lockFile.delete(); 309 } 310 } 311 312 //----------------------------------------------------------------------- 313 /** 314 * Write a character. 315 * @param idx the character to write 316 * @throws IOException if an I/O error occurs 317 */ 318 @Override 319 public void write(final int idx) throws IOException { 320 out.write(idx); 321 } 322 323 /** 324 * Write the characters from an array. 325 * @param chr the characters to write 326 * @throws IOException if an I/O error occurs 327 */ 328 @Override 329 public void write(final char[] chr) throws IOException { 330 out.write(chr); 331 } 332 333 /** 334 * Write the specified characters from an array. 335 * @param chr the characters to write 336 * @param st The start offset 337 * @param end The number of characters to write 338 * @throws IOException if an I/O error occurs 339 */ 340 @Override 341 public void write(final char[] chr, final int st, final int end) throws IOException { 342 out.write(chr, st, end); 343 } 344 345 /** 346 * Write the characters from a string. 347 * @param str the string to write 348 * @throws IOException if an I/O error occurs 349 */ 350 @Override 351 public void write(final String str) throws IOException { 352 out.write(str); 353 } 354 355 /** 356 * Write the specified characters from a string. 357 * @param str the string to write 358 * @param st The start offset 359 * @param end The number of characters to write 360 * @throws IOException if an I/O error occurs 361 */ 362 @Override 363 public void write(final String str, final int st, final int end) throws IOException { 364 out.write(str, st, end); 365 } 366 367 /** 368 * Flush the stream. 369 * @throws IOException if an I/O error occurs 370 */ 371 @Override 372 public void flush() throws IOException { 373 out.flush(); 374 } 375 376}