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} 034 * that will use a lock file to prevent duplicate writes. 035 * </p> 036 * <p> 037 * <b>Note:</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 lock file cannot be deleted, 040 * an exception is thrown. 041 * </p> 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}. 046 * The encoding may also be specified, and defaults to the platform default. 047 * </p> 048 */ 049public class LockableFileWriter extends Writer { 050 // Cannot extend ProxyWriter, as requires writer to be 051 // known when super() is called 052 053 /** The extension for the lock file. */ 054 private static final String LCK = ".lck"; 055 056 /** The writer to decorate. */ 057 private final Writer out; 058 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(final 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(final String fileName, final 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(final String fileName, final boolean append, final 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(final 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(final File file, final 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 * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead 132 */ 133 @Deprecated 134 public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException { 135 this(file, Charset.defaultCharset(), append, lockDir); 136 } 137 138 /** 139 * Constructs a LockableFileWriter with a file encoding. 140 * 141 * @param file the file to write to, not null 142 * @param charset the charset to use, null means platform default 143 * @throws NullPointerException if the file is null 144 * @throws IOException in case of an I/O error 145 * @since 2.3 146 */ 147 public LockableFileWriter(final File file, final Charset charset) throws IOException { 148 this(file, charset, false, null); 149 } 150 151 /** 152 * Constructs a LockableFileWriter with a file encoding. 153 * 154 * @param file the file to write to, not null 155 * @param charsetName the name of the requested charset, null means platform default 156 * @throws NullPointerException if the file is null 157 * @throws IOException in case of an I/O error 158 * @throws java.nio.charset.UnsupportedCharsetException 159 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 160 * supported. 161 */ 162 public LockableFileWriter(final File file, final String charsetName) throws IOException { 163 this(file, charsetName, false, null); 164 } 165 166 /** 167 * Constructs a LockableFileWriter with a file encoding. 168 * 169 * @param file the file to write to, not null 170 * @param charset the name of the requested charset, null means platform default 171 * @param append true if content should be appended, false to overwrite 172 * @param lockDir the directory in which the lock file should be held 173 * @throws NullPointerException if the file is null 174 * @throws IOException in case of an I/O error 175 * @since 2.3 176 */ 177 public LockableFileWriter(File file, final Charset charset, final boolean append, 178 String lockDir) throws IOException { 179 // init file to create/append 180 file = file.getAbsoluteFile(); 181 if (file.getParentFile() != null) { 182 FileUtils.forceMkdir(file.getParentFile()); 183 } 184 if (file.isDirectory()) { 185 throw new IOException("File specified is a directory"); 186 } 187 188 // init lock file 189 if (lockDir == null) { 190 lockDir = System.getProperty("java.io.tmpdir"); 191 } 192 final File lockDirFile = new File(lockDir); 193 FileUtils.forceMkdir(lockDirFile); 194 testLockDir(lockDirFile); 195 lockFile = new File(lockDirFile, file.getName() + LCK); 196 197 // check if locked 198 createLock(); 199 200 // init wrapped writer 201 out = initWriter(file, charset, append); 202 } 203 204 /** 205 * Constructs a LockableFileWriter with a file encoding. 206 * 207 * @param file the file to write to, not null 208 * @param charsetName the encoding to use, null means platform default 209 * @param append true if content should be appended, false to overwrite 210 * @param lockDir the directory in which the lock file should be held 211 * @throws NullPointerException if the file is null 212 * @throws IOException in case of an I/O error 213 * @throws java.nio.charset.UnsupportedCharsetException 214 * thrown instead of {@link java.io.UnsupportedEncodingException} in version 2.2 if the encoding is not 215 * supported. 216 */ 217 public LockableFileWriter(final File file, final String charsetName, final boolean append, 218 final String lockDir) throws IOException { 219 this(file, Charsets.toCharset(charsetName), append, lockDir); 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 * Initializes 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 initialized 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) { 274 FileUtils.deleteQuietly(file); 275 } 276 throw ex; 277 } 278 } 279 280 /** 281 * Closes the file writer and deletes the lock file. 282 * 283 * @throws IOException if an I/O error occurs. 284 */ 285 @Override 286 public void close() throws IOException { 287 try { 288 out.close(); 289 } finally { 290 FileUtils.delete(lockFile); 291 } 292 } 293 294 /** 295 * Writes a character. 296 * @param c the character to write 297 * @throws IOException if an I/O error occurs. 298 */ 299 @Override 300 public void write(final int c) throws IOException { 301 out.write(c); 302 } 303 304 /** 305 * Writes the characters from an array. 306 * @param cbuf the characters to write 307 * @throws IOException if an I/O error occurs. 308 */ 309 @Override 310 public void write(final char[] cbuf) throws IOException { 311 out.write(cbuf); 312 } 313 314 /** 315 * Writes the specified characters from an array. 316 * @param cbuf the characters to write 317 * @param off The start offset 318 * @param len The number of characters to write 319 * @throws IOException if an I/O error occurs. 320 */ 321 @Override 322 public void write(final char[] cbuf, final int off, final int len) throws IOException { 323 out.write(cbuf, off, len); 324 } 325 326 /** 327 * Writes the characters from a string. 328 * @param str the string to write 329 * @throws IOException if an I/O error occurs. 330 */ 331 @Override 332 public void write(final String str) throws IOException { 333 out.write(str); 334 } 335 336 /** 337 * Writes the specified characters from a string. 338 * @param str the string to write 339 * @param off The start offset 340 * @param len The number of characters to write 341 * @throws IOException if an I/O error occurs. 342 */ 343 @Override 344 public void write(final String str, final int off, final int len) throws IOException { 345 out.write(str, off, len); 346 } 347 348 /** 349 * Flushes the stream. 350 * @throws IOException if an I/O error occurs. 351 */ 352 @Override 353 public void flush() throws IOException { 354 out.flush(); 355 } 356 357}