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    }