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