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.Writer; 025import java.nio.charset.Charset; 026 027import org.apache.commons.io.Charsets; 028import org.apache.commons.io.FileUtils; 029import org.apache.commons.io.IOUtils; 030 031/** 032 * FileWriter that will create and honor lock files to allow simple 033 * cross thread file lock handling. 034 * <p> 035 * This class provides a simple alternative to <code>FileWriter</code> 036 * that will use a lock file to prevent duplicate writes. 037 * <p> 038 * <b>N.B.</b> the lock file is deleted when {@link #close()} is called 039 * - or if the main file cannot be opened initially. 040 * In the (unlikely) event that the lockfile cannot be deleted, 041 * this is not reported, and subsequent requests using 042 * the same lockfile will fail. 043 * <p> 044 * By default, the file will be overwritten, but this may be changed to append. 045 * The lock directory may be specified, but defaults to the system property 046 * <code>java.io.tmpdir</code>. 047 * The encoding may also be specified, and defaults to the platform default. 048 * 049 * @version $Id: LockableFileWriter.java 1686747 2015-06-21 18:44:49Z krosenvold $ 050 */ 051public class LockableFileWriter extends Writer { 052 // Cannot extend ProxyWriter, as requires writer to be 053 // known when super() is called 054 055 /** The extension for the lock file. */ 056 private static final String LCK = ".lck"; 057 058 /** The writer to decorate. */ 059 private final Writer out; 060 /** The lock file. */ 061 private final File lockFile; 062 063 /** 064 * Constructs a LockableFileWriter. 065 * If the file exists, it is overwritten. 066 * 067 * @param fileName the file to write to, not null 068 * @throws NullPointerException if the file is null 069 * @throws IOException in case of an I/O error 070 */ 071 public LockableFileWriter(final String fileName) throws IOException { 072 this(fileName, false, null); 073 } 074 075 /** 076 * Constructs a LockableFileWriter. 077 * 078 * @param fileName file to write to, not null 079 * @param append true if content should be appended, false to overwrite 080 * @throws NullPointerException if the file is null 081 * @throws IOException in case of an I/O error 082 */ 083 public LockableFileWriter(final String fileName, final boolean append) throws IOException { 084 this(fileName, append, null); 085 } 086 087 /** 088 * Constructs a LockableFileWriter. 089 * 090 * @param fileName the file to write to, not null 091 * @param append true if content should be appended, false to overwrite 092 * @param lockDir the directory in which the lock file should be held 093 * @throws NullPointerException if the file is null 094 * @throws IOException in case of an I/O error 095 */ 096 public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException { 097 this(new File(fileName), append, lockDir); 098 } 099 100 /** 101 * Constructs a LockableFileWriter. 102 * If the file exists, it is overwritten. 103 * 104 * @param file the file to write to, not null 105 * @throws NullPointerException if the file is null 106 * @throws IOException in case of an I/O error 107 */ 108 public LockableFileWriter(final File file) throws IOException { 109 this(file, false, null); 110 } 111 112 /** 113 * Constructs a LockableFileWriter. 114 * 115 * @param file the file to write to, not null 116 * @param append true if content should be appended, false to overwrite 117 * @throws NullPointerException if the file is null 118 * @throws IOException in case of an I/O error 119 */ 120 public LockableFileWriter(final File file, final boolean append) throws IOException { 121 this(file, append, null); 122 } 123 124 /** 125 * Constructs a LockableFileWriter. 126 * 127 * @param file the file to write to, not null 128 * @param append true if content should be appended, false to overwrite 129 * @param lockDir the directory in which the lock file should be held 130 * @throws NullPointerException if the file is null 131 * @throws IOException in case of an I/O error 132 * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead 133 */ 134 @Deprecated 135 public LockableFileWriter(final File file, final boolean append, final 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(final File file, final 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 java.nio.charset.UnsupportedCharsetException 160 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 161 * supported. 162 */ 163 public LockableFileWriter(final File file, final 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, final Charset encoding, final 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 final 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 java.nio.charset.UnsupportedCharsetException 216 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 217 * supported. 218 */ 219 public LockableFileWriter(final File file, final String encoding, final boolean append, 220 final 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(final 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(final File file, final Charset encoding, final boolean append) throws IOException { 269 final 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 (final 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 (final 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(final 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(final 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(final char[] chr, final int st, final 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(final 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(final String str, final int st, final 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}