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    }