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.Writer;
026
027 import org.apache.commons.io.FileUtils;
028 import org.apache.commons.io.IOUtils;
029
030 /**
031 * FileWriter that will create and honor lock files to allow simple
032 * cross thread file lock handling.
033 * <p>
034 * This class provides a simple alternative to <code>FileWriter</code>
035 * that will use a lock file to prevent duplicate writes.
036 * <p>
037 * <b>N.B.</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 lockfile cannot be deleted,
040 * this is not reported, and subsequent requests using
041 * the same lockfile will fail.
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</code>.
046 * The encoding may also be specified, and defaults to the platform default.
047 *
048 * @version $Id: LockableFileWriter.java 1302056 2012-03-18 03:03:38Z ggregory $
049 */
050 public class LockableFileWriter extends Writer {
051 // Cannot extend ProxyWriter, as requires writer to be
052 // known when super() is called
053
054 /** The extension for the lock file. */
055 private static final String LCK = ".lck";
056
057 /** The writer to decorate. */
058 private final Writer out;
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(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(String fileName, 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(String fileName, boolean append, 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(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(File file, 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 */
132 public LockableFileWriter(File file, boolean append, String lockDir) throws IOException {
133 this(file, null, 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 encoding the encoding 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 */
144 public LockableFileWriter(File file, String encoding) throws IOException {
145 this(file, encoding, false, null);
146 }
147
148 /**
149 * Constructs a LockableFileWriter with a file encoding.
150 *
151 * @param file the file to write to, not null
152 * @param encoding the encoding to use, null means platform default
153 * @param append true if content should be appended, false to overwrite
154 * @param lockDir the directory in which the lock file should be held
155 * @throws NullPointerException if the file is null
156 * @throws IOException in case of an I/O error
157 */
158 public LockableFileWriter(File file, String encoding, boolean append,
159 String lockDir) throws IOException {
160 super();
161 // init file to create/append
162 file = file.getAbsoluteFile();
163 if (file.getParentFile() != null) {
164 FileUtils.forceMkdir(file.getParentFile());
165 }
166 if (file.isDirectory()) {
167 throw new IOException("File specified is a directory");
168 }
169
170 // init lock file
171 if (lockDir == null) {
172 lockDir = System.getProperty("java.io.tmpdir");
173 }
174 File lockDirFile = new File(lockDir);
175 FileUtils.forceMkdir(lockDirFile);
176 testLockDir(lockDirFile);
177 lockFile = new File(lockDirFile, file.getName() + LCK);
178
179 // check if locked
180 createLock();
181
182 // init wrapped writer
183 out = initWriter(file, encoding, append);
184 }
185
186 //-----------------------------------------------------------------------
187 /**
188 * Tests that we can write to the lock directory.
189 *
190 * @param lockDir the File representing the lock directory
191 * @throws IOException if we cannot write to the lock directory
192 * @throws IOException if we cannot find the lock file
193 */
194 private void testLockDir(File lockDir) throws IOException {
195 if (!lockDir.exists()) {
196 throw new IOException(
197 "Could not find lockDir: " + lockDir.getAbsolutePath());
198 }
199 if (!lockDir.canWrite()) {
200 throw new IOException(
201 "Could not write to lockDir: " + lockDir.getAbsolutePath());
202 }
203 }
204
205 /**
206 * Creates the lock file.
207 *
208 * @throws IOException if we cannot create the file
209 */
210 private void createLock() throws IOException {
211 synchronized (LockableFileWriter.class) {
212 if (!lockFile.createNewFile()) {
213 throw new IOException("Can't write file, lock " +
214 lockFile.getAbsolutePath() + " exists");
215 }
216 lockFile.deleteOnExit();
217 }
218 }
219
220 /**
221 * Initialise the wrapped file writer.
222 * Ensure that a cleanup occurs if the writer creation fails.
223 *
224 * @param file the file to be accessed
225 * @param encoding the encoding to use
226 * @param append true to append
227 * @return The initialised writer
228 * @throws IOException if an error occurs
229 */
230 private Writer initWriter(File file, String encoding, boolean append) throws IOException {
231 boolean fileExistedAlready = file.exists();
232 OutputStream stream = null;
233 Writer writer = null;
234 try {
235 if (encoding == null) {
236 writer = new FileWriter(file.getAbsolutePath(), append);
237 } else {
238 stream = new FileOutputStream(file.getAbsolutePath(), append);
239 writer = new OutputStreamWriter(stream, encoding);
240 }
241 } catch (IOException ex) {
242 IOUtils.closeQuietly(writer);
243 IOUtils.closeQuietly(stream);
244 FileUtils.deleteQuietly(lockFile);
245 if (fileExistedAlready == false) {
246 FileUtils.deleteQuietly(file);
247 }
248 throw ex;
249 } catch (RuntimeException ex) {
250 IOUtils.closeQuietly(writer);
251 IOUtils.closeQuietly(stream);
252 FileUtils.deleteQuietly(lockFile);
253 if (fileExistedAlready == false) {
254 FileUtils.deleteQuietly(file);
255 }
256 throw ex;
257 }
258 return writer;
259 }
260
261 //-----------------------------------------------------------------------
262 /**
263 * Closes the file writer and deletes the lockfile (if possible).
264 *
265 * @throws IOException if an I/O error occurs
266 */
267 @Override
268 public void close() throws IOException {
269 try {
270 out.close();
271 } finally {
272 lockFile.delete();
273 }
274 }
275
276 //-----------------------------------------------------------------------
277 /**
278 * Write a character.
279 * @param idx the character to write
280 * @throws IOException if an I/O error occurs
281 */
282 @Override
283 public void write(int idx) throws IOException {
284 out.write(idx);
285 }
286
287 /**
288 * Write the characters from an array.
289 * @param chr the characters to write
290 * @throws IOException if an I/O error occurs
291 */
292 @Override
293 public void write(char[] chr) throws IOException {
294 out.write(chr);
295 }
296
297 /**
298 * Write the specified characters from an array.
299 * @param chr the characters to write
300 * @param st The start offset
301 * @param end The number of characters to write
302 * @throws IOException if an I/O error occurs
303 */
304 @Override
305 public void write(char[] chr, int st, int end) throws IOException {
306 out.write(chr, st, end);
307 }
308
309 /**
310 * Write the characters from a string.
311 * @param str the string to write
312 * @throws IOException if an I/O error occurs
313 */
314 @Override
315 public void write(String str) throws IOException {
316 out.write(str);
317 }
318
319 /**
320 * Write the specified characters from a string.
321 * @param str the string to write
322 * @param st The start offset
323 * @param end The number of characters to write
324 * @throws IOException if an I/O error occurs
325 */
326 @Override
327 public void write(String str, int st, int end) throws IOException {
328 out.write(str, st, end);
329 }
330
331 /**
332 * Flush the stream.
333 * @throws IOException if an I/O error occurs
334 */
335 @Override
336 public void flush() throws IOException {
337 out.flush();
338 }
339
340 }