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