1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.io.output;
18
19 import java.io.File;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.io.OutputStreamWriter;
24 import java.io.UnsupportedEncodingException;
25 import java.io.Writer;
26 import java.nio.charset.Charset;
27 import java.nio.charset.UnsupportedCharsetException;
28
29 import org.apache.commons.io.Charsets;
30 import org.apache.commons.io.FileUtils;
31 import org.apache.commons.io.IOUtils;
32
33 /**
34 * FileWriter that will create and honor lock files to allow simple
35 * cross thread file lock handling.
36 * <p>
37 * This class provides a simple alternative to <code>FileWriter</code>
38 * that will use a lock file to prevent duplicate writes.
39 * <p>
40 * <b>N.B.</b> the lock file is deleted when {@link #close()} is called
41 * - or if the main file cannot be opened initially.
42 * In the (unlikely) event that the lockfile cannot be deleted,
43 * this is not reported, and subsequent requests using
44 * the same lockfile will fail.
45 * <p>
46 * By default, the file will be overwritten, but this may be changed to append.
47 * The lock directory may be specified, but defaults to the system property
48 * <code>java.io.tmpdir</code>.
49 * The encoding may also be specified, and defaults to the platform default.
50 *
51 * @version $Id: LockableFileWriter.java 1471767 2013-04-24 23:24:19Z sebb $
52 */
53 public class LockableFileWriter extends Writer {
54 // Cannot extend ProxyWriter, as requires writer to be
55 // known when super() is called
56
57 /** The extension for the lock file. */
58 private static final String LCK = ".lck";
59
60 /** The writer to decorate. */
61 private final Writer out;
62 /** The lock file. */
63 private final File lockFile;
64
65 /**
66 * Constructs a LockableFileWriter.
67 * If the file exists, it is overwritten.
68 *
69 * @param fileName the file to write to, not null
70 * @throws NullPointerException if the file is null
71 * @throws IOException in case of an I/O error
72 */
73 public LockableFileWriter(final String fileName) throws IOException {
74 this(fileName, false, null);
75 }
76
77 /**
78 * Constructs a LockableFileWriter.
79 *
80 * @param fileName file to write to, not null
81 * @param append true if content should be appended, false to overwrite
82 * @throws NullPointerException if the file is null
83 * @throws IOException in case of an I/O error
84 */
85 public LockableFileWriter(final String fileName, final boolean append) throws IOException {
86 this(fileName, append, null);
87 }
88
89 /**
90 * Constructs a LockableFileWriter.
91 *
92 * @param fileName the file to write to, not null
93 * @param append true if content should be appended, false to overwrite
94 * @param lockDir the directory in which the lock file should be held
95 * @throws NullPointerException if the file is null
96 * @throws IOException in case of an I/O error
97 */
98 public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException {
99 this(new File(fileName), append, lockDir);
100 }
101
102 /**
103 * Constructs a LockableFileWriter.
104 * If the file exists, it is overwritten.
105 *
106 * @param file the file to write to, not null
107 * @throws NullPointerException if the file is null
108 * @throws IOException in case of an I/O error
109 */
110 public LockableFileWriter(final File file) throws IOException {
111 this(file, false, null);
112 }
113
114 /**
115 * Constructs a LockableFileWriter.
116 *
117 * @param file the file to write to, not null
118 * @param append true if content should be appended, false to overwrite
119 * @throws NullPointerException if the file is null
120 * @throws IOException in case of an I/O error
121 */
122 public LockableFileWriter(final File file, final boolean append) throws IOException {
123 this(file, append, null);
124 }
125
126 /**
127 * Constructs a LockableFileWriter.
128 *
129 * @param file the file to write to, not null
130 * @param append true if content should be appended, false to overwrite
131 * @param lockDir the directory in which the lock file should be held
132 * @throws NullPointerException if the file is null
133 * @throws IOException in case of an I/O error
134 * @deprecated 2.5 use {@link #LockableFileWriter(File, Charset, boolean, String)} instead
135 */
136 @Deprecated
137 public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException {
138 this(file, Charset.defaultCharset(), append, lockDir);
139 }
140
141 /**
142 * Constructs a LockableFileWriter with a file encoding.
143 *
144 * @param file the file to write to, not null
145 * @param encoding the encoding to use, null means platform default
146 * @throws NullPointerException if the file is null
147 * @throws IOException in case of an I/O error
148 * @since 2.3
149 */
150 public LockableFileWriter(final File file, final Charset encoding) throws IOException {
151 this(file, encoding, false, null);
152 }
153
154 /**
155 * Constructs a LockableFileWriter with a file encoding.
156 *
157 * @param file the file to write to, not null
158 * @param encoding the encoding to use, null means platform default
159 * @throws NullPointerException if the file is null
160 * @throws IOException in case of an I/O error
161 * @throws UnsupportedCharsetException
162 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
163 * supported.
164 */
165 public LockableFileWriter(final File file, final String encoding) throws IOException {
166 this(file, encoding, false, null);
167 }
168
169 /**
170 * Constructs a LockableFileWriter with a file encoding.
171 *
172 * @param file the file to write to, not null
173 * @param encoding the encoding to use, null means platform default
174 * @param append true if content should be appended, false to overwrite
175 * @param lockDir the directory in which the lock file should be held
176 * @throws NullPointerException if the file is null
177 * @throws IOException in case of an I/O error
178 * @since 2.3
179 */
180 public LockableFileWriter(File file, final Charset encoding, final boolean append,
181 String lockDir) throws IOException {
182 super();
183 // init file to create/append
184 file = file.getAbsoluteFile();
185 if (file.getParentFile() != null) {
186 FileUtils.forceMkdir(file.getParentFile());
187 }
188 if (file.isDirectory()) {
189 throw new IOException("File specified is a directory");
190 }
191
192 // init lock file
193 if (lockDir == null) {
194 lockDir = System.getProperty("java.io.tmpdir");
195 }
196 final File lockDirFile = new File(lockDir);
197 FileUtils.forceMkdir(lockDirFile);
198 testLockDir(lockDirFile);
199 lockFile = new File(lockDirFile, file.getName() + LCK);
200
201 // check if locked
202 createLock();
203
204 // init wrapped writer
205 out = initWriter(file, encoding, append);
206 }
207
208 /**
209 * Constructs a LockableFileWriter with a file encoding.
210 *
211 * @param file the file to write to, not null
212 * @param encoding the encoding to use, null means platform default
213 * @param append true if content should be appended, false to overwrite
214 * @param lockDir the directory in which the lock file should be held
215 * @throws NullPointerException if the file is null
216 * @throws IOException in case of an I/O error
217 * @throws UnsupportedCharsetException
218 * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
219 * supported.
220 */
221 public LockableFileWriter(final File file, final String encoding, final boolean append,
222 final String lockDir) throws IOException {
223 this(file, Charsets.toCharset(encoding), append, lockDir);
224 }
225
226 //-----------------------------------------------------------------------
227 /**
228 * Tests that we can write to the lock directory.
229 *
230 * @param lockDir the File representing the lock directory
231 * @throws IOException if we cannot write to the lock directory
232 * @throws IOException if we cannot find the lock file
233 */
234 private void testLockDir(final File lockDir) throws IOException {
235 if (!lockDir.exists()) {
236 throw new IOException(
237 "Could not find lockDir: " + lockDir.getAbsolutePath());
238 }
239 if (!lockDir.canWrite()) {
240 throw new IOException(
241 "Could not write to lockDir: " + lockDir.getAbsolutePath());
242 }
243 }
244
245 /**
246 * Creates the lock file.
247 *
248 * @throws IOException if we cannot create the file
249 */
250 private void createLock() throws IOException {
251 synchronized (LockableFileWriter.class) {
252 if (!lockFile.createNewFile()) {
253 throw new IOException("Can't write file, lock " +
254 lockFile.getAbsolutePath() + " exists");
255 }
256 lockFile.deleteOnExit();
257 }
258 }
259
260 /**
261 * Initialise the wrapped file writer.
262 * Ensure that a cleanup occurs if the writer creation fails.
263 *
264 * @param file the file to be accessed
265 * @param encoding the encoding to use
266 * @param append true to append
267 * @return The initialised writer
268 * @throws IOException if an error occurs
269 */
270 private Writer initWriter(final File file, final Charset encoding, final boolean append) throws IOException {
271 final boolean fileExistedAlready = file.exists();
272 OutputStream stream = null;
273 Writer writer = null;
274 try {
275 stream = new FileOutputStream(file.getAbsolutePath(), append);
276 writer = new OutputStreamWriter(stream, Charsets.toCharset(encoding));
277 } catch (final IOException ex) {
278 IOUtils.closeQuietly(writer);
279 IOUtils.closeQuietly(stream);
280 FileUtils.deleteQuietly(lockFile);
281 if (fileExistedAlready == false) {
282 FileUtils.deleteQuietly(file);
283 }
284 throw ex;
285 } catch (final RuntimeException ex) {
286 IOUtils.closeQuietly(writer);
287 IOUtils.closeQuietly(stream);
288 FileUtils.deleteQuietly(lockFile);
289 if (fileExistedAlready == false) {
290 FileUtils.deleteQuietly(file);
291 }
292 throw ex;
293 }
294 return writer;
295 }
296
297 //-----------------------------------------------------------------------
298 /**
299 * Closes the file writer and deletes the lockfile (if possible).
300 *
301 * @throws IOException if an I/O error occurs
302 */
303 @Override
304 public void close() throws IOException {
305 try {
306 out.close();
307 } finally {
308 lockFile.delete();
309 }
310 }
311
312 //-----------------------------------------------------------------------
313 /**
314 * Write a character.
315 * @param idx the character to write
316 * @throws IOException if an I/O error occurs
317 */
318 @Override
319 public void write(final int idx) throws IOException {
320 out.write(idx);
321 }
322
323 /**
324 * Write the characters from an array.
325 * @param chr the characters to write
326 * @throws IOException if an I/O error occurs
327 */
328 @Override
329 public void write(final char[] chr) throws IOException {
330 out.write(chr);
331 }
332
333 /**
334 * Write the specified characters from an array.
335 * @param chr the characters to write
336 * @param st The start offset
337 * @param end The number of characters to write
338 * @throws IOException if an I/O error occurs
339 */
340 @Override
341 public void write(final char[] chr, final int st, final int end) throws IOException {
342 out.write(chr, st, end);
343 }
344
345 /**
346 * Write the characters from a string.
347 * @param str the string to write
348 * @throws IOException if an I/O error occurs
349 */
350 @Override
351 public void write(final String str) throws IOException {
352 out.write(str);
353 }
354
355 /**
356 * Write the specified characters from a string.
357 * @param str the string to write
358 * @param st The start offset
359 * @param end The number of characters to write
360 * @throws IOException if an I/O error occurs
361 */
362 @Override
363 public void write(final String str, final int st, final int end) throws IOException {
364 out.write(str, st, end);
365 }
366
367 /**
368 * Flush the stream.
369 * @throws IOException if an I/O error occurs
370 */
371 @Override
372 public void flush() throws IOException {
373 out.flush();
374 }
375
376 }