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 * @author <a href="mailto:sanders@apache.org">Scott Sanders</a>
049 * @author <a href="mailto:ms@collab.net">Michael Salmon</a>
050 * @author <a href="mailto:jon@collab.net">Jon S. Stevens</a>
051 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
052 * @author Stephen Colebourne
053 * @author Andy Lehane
054 * @version $Id: LockableFileWriter.java 1002182 2010-09-28 14:39:19Z sebb $
055 */
056 public class LockableFileWriter extends Writer {
057 // Cannot extend ProxyWriter, as requires writer to be
058 // known when super() is called
059
060 /** The extension for the lock file. */
061 private static final String LCK = ".lck";
062
063 /** The writer to decorate. */
064 private final Writer out;
065 /** The lock file. */
066 private final File lockFile;
067
068 /**
069 * Constructs a LockableFileWriter.
070 * If the file exists, it is overwritten.
071 *
072 * @param fileName the file to write to, not null
073 * @throws NullPointerException if the file is null
074 * @throws IOException in case of an I/O error
075 */
076 public LockableFileWriter(String fileName) throws IOException {
077 this(fileName, false, null);
078 }
079
080 /**
081 * Constructs a LockableFileWriter.
082 *
083 * @param fileName file to write to, not null
084 * @param append true if content should be appended, false to overwrite
085 * @throws NullPointerException if the file is null
086 * @throws IOException in case of an I/O error
087 */
088 public LockableFileWriter(String fileName, boolean append) throws IOException {
089 this(fileName, append, null);
090 }
091
092 /**
093 * Constructs a LockableFileWriter.
094 *
095 * @param fileName the file to write to, not null
096 * @param append true if content should be appended, false to overwrite
097 * @param lockDir the directory in which the lock file should be held
098 * @throws NullPointerException if the file is null
099 * @throws IOException in case of an I/O error
100 */
101 public LockableFileWriter(String fileName, boolean append, String lockDir) throws IOException {
102 this(new File(fileName), append, lockDir);
103 }
104
105 /**
106 * Constructs a LockableFileWriter.
107 * If the file exists, it is overwritten.
108 *
109 * @param file the file to write to, not null
110 * @throws NullPointerException if the file is null
111 * @throws IOException in case of an I/O error
112 */
113 public LockableFileWriter(File file) throws IOException {
114 this(file, false, null);
115 }
116
117 /**
118 * Constructs a LockableFileWriter.
119 *
120 * @param file the file to write to, not null
121 * @param append true if content should be appended, false to overwrite
122 * @throws NullPointerException if the file is null
123 * @throws IOException in case of an I/O error
124 */
125 public LockableFileWriter(File file, boolean append) throws IOException {
126 this(file, append, null);
127 }
128
129 /**
130 * Constructs a LockableFileWriter.
131 *
132 * @param file the file to write to, not null
133 * @param append true if content should be appended, false to overwrite
134 * @param lockDir the directory in which the lock file should be held
135 * @throws NullPointerException if the file is null
136 * @throws IOException in case of an I/O error
137 */
138 public LockableFileWriter(File file, boolean append, String lockDir) throws IOException {
139 this(file, null, append, lockDir);
140 }
141
142 /**
143 * Constructs a LockableFileWriter with a file encoding.
144 *
145 * @param file the file to write to, not null
146 * @param encoding the encoding to use, null means platform default
147 * @throws NullPointerException if the file is null
148 * @throws IOException in case of an I/O error
149 */
150 public LockableFileWriter(File file, String 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 * @param append true if content should be appended, false to overwrite
160 * @param lockDir the directory in which the lock file should be held
161 * @throws NullPointerException if the file is null
162 * @throws IOException in case of an I/O error
163 */
164 public LockableFileWriter(File file, String encoding, boolean append,
165 String lockDir) throws IOException {
166 super();
167 // init file to create/append
168 file = file.getAbsoluteFile();
169 if (file.getParentFile() != null) {
170 FileUtils.forceMkdir(file.getParentFile());
171 }
172 if (file.isDirectory()) {
173 throw new IOException("File specified is a directory");
174 }
175
176 // init lock file
177 if (lockDir == null) {
178 lockDir = System.getProperty("java.io.tmpdir");
179 }
180 File lockDirFile = new File(lockDir);
181 FileUtils.forceMkdir(lockDirFile);
182 testLockDir(lockDirFile);
183 lockFile = new File(lockDirFile, file.getName() + LCK);
184
185 // check if locked
186 createLock();
187
188 // init wrapped writer
189 out = initWriter(file, encoding, append);
190 }
191
192 //-----------------------------------------------------------------------
193 /**
194 * Tests that we can write to the lock directory.
195 *
196 * @param lockDir the File representing the lock directory
197 * @throws IOException if we cannot write to the lock directory
198 * @throws IOException if we cannot find the lock file
199 */
200 private void testLockDir(File lockDir) throws IOException {
201 if (!lockDir.exists()) {
202 throw new IOException(
203 "Could not find lockDir: " + lockDir.getAbsolutePath());
204 }
205 if (!lockDir.canWrite()) {
206 throw new IOException(
207 "Could not write to lockDir: " + lockDir.getAbsolutePath());
208 }
209 }
210
211 /**
212 * Creates the lock file.
213 *
214 * @throws IOException if we cannot create the file
215 */
216 private void createLock() throws IOException {
217 synchronized (LockableFileWriter.class) {
218 if (!lockFile.createNewFile()) {
219 throw new IOException("Can't write file, lock " +
220 lockFile.getAbsolutePath() + " exists");
221 }
222 lockFile.deleteOnExit();
223 }
224 }
225
226 /**
227 * Initialise the wrapped file writer.
228 * Ensure that a cleanup occurs if the writer creation fails.
229 *
230 * @param file the file to be accessed
231 * @param encoding the encoding to use
232 * @param append true to append
233 * @return The initialised writer
234 * @throws IOException if an error occurs
235 */
236 private Writer initWriter(File file, String encoding, boolean append) throws IOException {
237 boolean fileExistedAlready = file.exists();
238 OutputStream stream = null;
239 Writer writer = null;
240 try {
241 if (encoding == null) {
242 writer = new FileWriter(file.getAbsolutePath(), append);
243 } else {
244 stream = new FileOutputStream(file.getAbsolutePath(), append);
245 writer = new OutputStreamWriter(stream, encoding);
246 }
247 } catch (IOException ex) {
248 IOUtils.closeQuietly(writer);
249 IOUtils.closeQuietly(stream);
250 FileUtils.deleteQuietly(lockFile);
251 if (fileExistedAlready == false) {
252 FileUtils.deleteQuietly(file);
253 }
254 throw ex;
255 } catch (RuntimeException ex) {
256 IOUtils.closeQuietly(writer);
257 IOUtils.closeQuietly(stream);
258 FileUtils.deleteQuietly(lockFile);
259 if (fileExistedAlready == false) {
260 FileUtils.deleteQuietly(file);
261 }
262 throw ex;
263 }
264 return writer;
265 }
266
267 //-----------------------------------------------------------------------
268 /**
269 * Closes the file writer and deletes the lockfile (if possible).
270 *
271 * @throws IOException if an I/O error occurs
272 */
273 @Override
274 public void close() throws IOException {
275 try {
276 out.close();
277 } finally {
278 lockFile.delete();
279 }
280 }
281
282 //-----------------------------------------------------------------------
283 /**
284 * Write a character.
285 * @param idx the character to write
286 * @throws IOException if an I/O error occurs
287 */
288 @Override
289 public void write(int idx) throws IOException {
290 out.write(idx);
291 }
292
293 /**
294 * Write the characters from an array.
295 * @param chr the characters to write
296 * @throws IOException if an I/O error occurs
297 */
298 @Override
299 public void write(char[] chr) throws IOException {
300 out.write(chr);
301 }
302
303 /**
304 * Write the specified characters from an array.
305 * @param chr the characters to write
306 * @param st The start offset
307 * @param end The number of characters to write
308 * @throws IOException if an I/O error occurs
309 */
310 @Override
311 public void write(char[] chr, int st, int end) throws IOException {
312 out.write(chr, st, end);
313 }
314
315 /**
316 * Write the characters from a string.
317 * @param str the string to write
318 * @throws IOException if an I/O error occurs
319 */
320 @Override
321 public void write(String str) throws IOException {
322 out.write(str);
323 }
324
325 /**
326 * Write the specified characters from a string.
327 * @param str the string to write
328 * @param st The start offset
329 * @param end The number of characters to write
330 * @throws IOException if an I/O error occurs
331 */
332 @Override
333 public void write(String str, int st, int end) throws IOException {
334 out.write(str, st, end);
335 }
336
337 /**
338 * Flush the stream.
339 * @throws IOException if an I/O error occurs
340 */
341 @Override
342 public void flush() throws IOException {
343 out.flush();
344 }
345
346 }