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