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