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.FileWriter;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.io.OutputStreamWriter;
25 import java.io.Writer;
26 
27 import org.apache.commons.io.FileUtils;
28 import org.apache.commons.io.IOUtils;
29 
30 /**
31  * FileWriter that will create and honor lock files to allow simple
32  * cross thread file lock handling.
33  * <p>
34  * This class provides a simple alternative to <code>FileWriter</code>
35  * that will use a lock file to prevent duplicate writes.
36  * <p>
37  * By default, the file will be overwritten, but this may be changed to append.
38  * The lock directory may be specified, but defaults to the system property
39  * <code>java.io.tmpdir</code>.
40  * The encoding may also be specified, and defaults to the platform default.
41  *
42  * @author <a href="mailto:sanders@apache.org">Scott Sanders</a>
43  * @author <a href="mailto:ms@collab.net">Michael Salmon</a>
44  * @author <a href="mailto:jon@collab.net">Jon S. Stevens</a>
45  * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
46  * @author Stephen Colebourne
47  * @author Andy Lehane
48  * @version $Id: LockableFileWriter.java 610010 2008-01-08 14:50:59Z niallp $
49  */
50 public class LockableFileWriter extends Writer {
51     // Cannot extend ProxyWriter, as requires writer to be
52     // known when super() is called
53 
54     /** The extension for the lock file. */
55     private static final String LCK = ".lck";
56 
57     /** The writer to decorate. */
58     private final Writer out;
59     /** The lock file. */
60     private final File lockFile;
61 
62     /**
63      * Constructs a LockableFileWriter.
64      * If the file exists, it is overwritten.
65      *
66      * @param fileName  the file to write to, not null
67      * @throws NullPointerException if the file is null
68      * @throws IOException in case of an I/O error
69      */
LockableFileWriter(String fileName)70     public LockableFileWriter(String fileName) throws IOException {
71         this(fileName, false, null);
72     }
73 
74     /**
75      * Constructs a LockableFileWriter.
76      *
77      * @param fileName  file to write to, not null
78      * @param append  true if content should be appended, false to overwrite
79      * @throws NullPointerException if the file is null
80      * @throws IOException in case of an I/O error
81      */
LockableFileWriter(String fileName, boolean append)82     public LockableFileWriter(String fileName, boolean append) throws IOException {
83         this(fileName, append, null);
84     }
85 
86     /**
87      * Constructs a LockableFileWriter.
88      *
89      * @param fileName  the file to write to, not null
90      * @param append  true if content should be appended, false to overwrite
91      * @param lockDir  the directory in which the lock file should be held
92      * @throws NullPointerException if the file is null
93      * @throws IOException in case of an I/O error
94      */
LockableFileWriter(String fileName, boolean append, String lockDir)95     public LockableFileWriter(String fileName, boolean append, String lockDir) throws IOException {
96         this(new File(fileName), append, lockDir);
97     }
98 
99     /**
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      */
LockableFileWriter(File file)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      */
LockableFileWriter(File file, boolean append)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      */
LockableFileWriter(File file, boolean append, String lockDir)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      */
LockableFileWriter(File file, String encoding)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      */
LockableFileWriter(File file, String encoding, boolean append, String lockDir)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      */
testLockDir(File lockDir)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      */
createLock()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      */
initWriter(File file, String encoding, boolean append)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             lockFile.delete();
245             if (fileExistedAlready == false) {
246                 file.delete();
247             }
248             throw ex;
249         } catch (RuntimeException ex) {
250             IOUtils.closeQuietly(writer);
251             IOUtils.closeQuietly(stream);
252             lockFile.delete();
253             if (fileExistedAlready == false) {
254                 file.delete();
255             }
256             throw ex;
257         }
258         return writer;
259     }
260 
261     //-----------------------------------------------------------------------
262     /**
263      * Closes the file writer.
264      *
265      * @throws IOException if an I/O error occurs
266      */
close()267     public void close() throws IOException {
268         try {
269             out.close();
270         } finally {
271             lockFile.delete();
272         }
273     }
274 
275     //-----------------------------------------------------------------------
276     /**
277      * Write a character.
278      * @param idx the character to write
279      * @throws IOException if an I/O error occurs
280      */
write(int idx)281     public void write(int idx) throws IOException {
282         out.write(idx);
283     }
284 
285     /**
286      * Write the characters from an array.
287      * @param chr the characters to write
288      * @throws IOException if an I/O error occurs
289      */
write(char[] chr)290     public void write(char[] chr) throws IOException {
291         out.write(chr);
292     }
293 
294     /**
295      * Write the specified characters from an array.
296      * @param chr the characters to write
297      * @param st The start offset
298      * @param end The number of characters to write
299      * @throws IOException if an I/O error occurs
300      */
write(char[] chr, int st, int end)301     public void write(char[] chr, int st, int end) throws IOException {
302         out.write(chr, st, end);
303     }
304 
305     /**
306      * Write the characters from a string.
307      * @param str the string to write
308      * @throws IOException if an I/O error occurs
309      */
write(String str)310     public void write(String str) throws IOException {
311         out.write(str);
312     }
313 
314     /**
315      * Write the specified characters from a string.
316      * @param str the string to write
317      * @param st The start offset
318      * @param end The number of characters to write
319      * @throws IOException if an I/O error occurs
320      */
write(String str, int st, int end)321     public void write(String str, int st, int end) throws IOException {
322         out.write(str, st, end);
323     }
324 
325     /**
326      * Flush the stream.
327      * @throws IOException if an I/O error occurs
328      */
flush()329     public void flush() throws IOException {
330         out.flush();
331     }
332 
333 }
334