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 */ 017 package org.apache.commons.io.output; 018 019 import java.io.File; 020 import java.io.FileOutputStream; 021 import java.io.IOException; 022 import java.io.OutputStream; 023 import java.io.OutputStreamWriter; 024 import java.io.UnsupportedEncodingException; 025 import java.io.Writer; 026 import java.nio.charset.Charset; 027 import java.nio.charset.UnsupportedCharsetException; 028 029 import org.apache.commons.io.Charsets; 030 import org.apache.commons.io.FileUtils; 031 import 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 1347574 2012-06-07 11:20:39Z sebb $ 052 */ 053 public 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(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(String fileName, 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(String fileName, boolean append, 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(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(File file, 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 */ 135 public LockableFileWriter(File file, boolean append, String lockDir) throws IOException { 136 this(file, Charset.defaultCharset(), append, lockDir); 137 } 138 139 /** 140 * Constructs a LockableFileWriter with a file encoding. 141 * 142 * @param file the file to write to, not null 143 * @param encoding the encoding to use, null means platform default 144 * @throws NullPointerException if the file is null 145 * @throws IOException in case of an I/O error 146 * @since 2.3 147 */ 148 public LockableFileWriter(File file, Charset encoding) throws IOException { 149 this(file, encoding, false, null); 150 } 151 152 /** 153 * Constructs a LockableFileWriter with a file encoding. 154 * 155 * @param file the file to write to, not null 156 * @param encoding the encoding to use, null means platform default 157 * @throws NullPointerException if the file is null 158 * @throws IOException in case of an I/O error 159 * @throws UnsupportedCharsetException 160 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not 161 * supported. 162 */ 163 public LockableFileWriter(File file, String encoding) throws IOException { 164 this(file, encoding, 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 encoding 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 * @since 2.3 177 */ 178 public LockableFileWriter(File file, Charset encoding, boolean append, 179 String lockDir) throws IOException { 180 super(); 181 // init file to create/append 182 file = file.getAbsoluteFile(); 183 if (file.getParentFile() != null) { 184 FileUtils.forceMkdir(file.getParentFile()); 185 } 186 if (file.isDirectory()) { 187 throw new IOException("File specified is a directory"); 188 } 189 190 // init lock file 191 if (lockDir == null) { 192 lockDir = System.getProperty("java.io.tmpdir"); 193 } 194 File lockDirFile = new File(lockDir); 195 FileUtils.forceMkdir(lockDirFile); 196 testLockDir(lockDirFile); 197 lockFile = new File(lockDirFile, file.getName() + LCK); 198 199 // check if locked 200 createLock(); 201 202 // init wrapped writer 203 out = initWriter(file, encoding, append); 204 } 205 206 /** 207 * Constructs a LockableFileWriter with a file encoding. 208 * 209 * @param file the file to write to, not null 210 * @param encoding the encoding to use, null means platform default 211 * @param append true if content should be appended, false to overwrite 212 * @param lockDir the directory in which the lock file should be held 213 * @throws NullPointerException if the file is null 214 * @throws IOException in case of an I/O error 215 * @throws UnsupportedCharsetException 216 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not 217 * supported. 218 */ 219 public LockableFileWriter(File file, String encoding, boolean append, 220 String lockDir) throws IOException { 221 this(file, Charsets.toCharset(encoding), append, lockDir); 222 } 223 224 //----------------------------------------------------------------------- 225 /** 226 * Tests that we can write to the lock directory. 227 * 228 * @param lockDir the File representing the lock directory 229 * @throws IOException if we cannot write to the lock directory 230 * @throws IOException if we cannot find the lock file 231 */ 232 private void testLockDir(File lockDir) throws IOException { 233 if (!lockDir.exists()) { 234 throw new IOException( 235 "Could not find lockDir: " + lockDir.getAbsolutePath()); 236 } 237 if (!lockDir.canWrite()) { 238 throw new IOException( 239 "Could not write to lockDir: " + lockDir.getAbsolutePath()); 240 } 241 } 242 243 /** 244 * Creates the lock file. 245 * 246 * @throws IOException if we cannot create the file 247 */ 248 private void createLock() throws IOException { 249 synchronized (LockableFileWriter.class) { 250 if (!lockFile.createNewFile()) { 251 throw new IOException("Can't write file, lock " + 252 lockFile.getAbsolutePath() + " exists"); 253 } 254 lockFile.deleteOnExit(); 255 } 256 } 257 258 /** 259 * Initialise the wrapped file writer. 260 * Ensure that a cleanup occurs if the writer creation fails. 261 * 262 * @param file the file to be accessed 263 * @param encoding the encoding to use 264 * @param append true to append 265 * @return The initialised writer 266 * @throws IOException if an error occurs 267 */ 268 private Writer initWriter(File file, Charset encoding, boolean append) throws IOException { 269 boolean fileExistedAlready = file.exists(); 270 OutputStream stream = null; 271 Writer writer = null; 272 try { 273 stream = new FileOutputStream(file.getAbsolutePath(), append); 274 writer = new OutputStreamWriter(stream, Charsets.toCharset(encoding)); 275 } catch (IOException ex) { 276 IOUtils.closeQuietly(writer); 277 IOUtils.closeQuietly(stream); 278 FileUtils.deleteQuietly(lockFile); 279 if (fileExistedAlready == false) { 280 FileUtils.deleteQuietly(file); 281 } 282 throw ex; 283 } catch (RuntimeException ex) { 284 IOUtils.closeQuietly(writer); 285 IOUtils.closeQuietly(stream); 286 FileUtils.deleteQuietly(lockFile); 287 if (fileExistedAlready == false) { 288 FileUtils.deleteQuietly(file); 289 } 290 throw ex; 291 } 292 return writer; 293 } 294 295 //----------------------------------------------------------------------- 296 /** 297 * Closes the file writer and deletes the lockfile (if possible). 298 * 299 * @throws IOException if an I/O error occurs 300 */ 301 @Override 302 public void close() throws IOException { 303 try { 304 out.close(); 305 } finally { 306 lockFile.delete(); 307 } 308 } 309 310 //----------------------------------------------------------------------- 311 /** 312 * Write a character. 313 * @param idx the character to write 314 * @throws IOException if an I/O error occurs 315 */ 316 @Override 317 public void write(int idx) throws IOException { 318 out.write(idx); 319 } 320 321 /** 322 * Write the characters from an array. 323 * @param chr the characters to write 324 * @throws IOException if an I/O error occurs 325 */ 326 @Override 327 public void write(char[] chr) throws IOException { 328 out.write(chr); 329 } 330 331 /** 332 * Write the specified characters from an array. 333 * @param chr the characters to write 334 * @param st The start offset 335 * @param end The number of characters to write 336 * @throws IOException if an I/O error occurs 337 */ 338 @Override 339 public void write(char[] chr, int st, int end) throws IOException { 340 out.write(chr, st, end); 341 } 342 343 /** 344 * Write the characters from a string. 345 * @param str the string to write 346 * @throws IOException if an I/O error occurs 347 */ 348 @Override 349 public void write(String str) throws IOException { 350 out.write(str); 351 } 352 353 /** 354 * Write the specified characters from a string. 355 * @param str the string to write 356 * @param st The start offset 357 * @param end The number of characters to write 358 * @throws IOException if an I/O error occurs 359 */ 360 @Override 361 public void write(String str, int st, int end) throws IOException { 362 out.write(str, st, end); 363 } 364 365 /** 366 * Flush the stream. 367 * @throws IOException if an I/O error occurs 368 */ 369 @Override 370 public void flush() throws IOException { 371 out.flush(); 372 } 373 374 }