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 }