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.UnsupportedEncodingException;
026    import java.io.Writer;
027    import java.nio.charset.Charset;
028    import java.nio.charset.UnsupportedCharsetException;
029    
030    import org.apache.commons.io.Charsets;
031    import org.apache.commons.io.FileUtils;
032    import org.apache.commons.io.IOUtils;
033    
034    /**
035     * FileWriter that will create and honor lock files to allow simple
036     * cross thread file lock handling.
037     * <p>
038     * This class provides a simple alternative to <code>FileWriter</code>
039     * that will use a lock file to prevent duplicate writes.
040     * <p>
041     * <b>N.B.</b> the lock file is deleted when {@link #close()} is called
042     * - or if the main file cannot be opened initially.
043     * In the (unlikely) event that the lockfile cannot be deleted, 
044     * this is not reported, and subsequent requests using
045     * the same lockfile will fail.
046     * <p>
047     * By default, the file will be overwritten, but this may be changed to append.
048     * The lock directory may be specified, but defaults to the system property
049     * <code>java.io.tmpdir</code>.
050     * The encoding may also be specified, and defaults to the platform default.
051     *
052     * @version $Id: LockableFileWriter.java 1311756 2012-04-10 14:32:42Z ggregory $
053     */
054    public class LockableFileWriter extends Writer {
055        // Cannot extend ProxyWriter, as requires writer to be
056        // known when super() is called
057    
058        /** The extension for the lock file. */
059        private static final String LCK = ".lck";
060    
061        /** The writer to decorate. */
062        private final Writer out;
063        /** The lock file. */
064        private final File lockFile;
065    
066        /**
067         * Constructs a LockableFileWriter.
068         * If the file exists, it is overwritten.
069         *
070         * @param fileName  the file to write to, not null
071         * @throws NullPointerException if the file is null
072         * @throws IOException in case of an I/O error
073         */
074        public LockableFileWriter(String fileName) throws IOException {
075            this(fileName, false, null);
076        }
077    
078        /**
079         * Constructs a LockableFileWriter.
080         *
081         * @param fileName  file to write to, not null
082         * @param append  true if content should be appended, false to overwrite
083         * @throws NullPointerException if the file is null
084         * @throws IOException in case of an I/O error
085         */
086        public LockableFileWriter(String fileName, boolean append) throws IOException {
087            this(fileName, append, null);
088        }
089    
090        /**
091         * Constructs a LockableFileWriter.
092         *
093         * @param fileName  the file to write to, not null
094         * @param append  true if content should be appended, false to overwrite
095         * @param lockDir  the directory in which the lock file should be held
096         * @throws NullPointerException if the file is null
097         * @throws IOException in case of an I/O error
098         */
099        public LockableFileWriter(String fileName, boolean append, String lockDir) throws IOException {
100            this(new File(fileName), append, lockDir);
101        }
102    
103        /**
104         * Constructs a LockableFileWriter.
105         * If the file exists, it is overwritten.
106         *
107         * @param file  the file to write to, not null
108         * @throws NullPointerException if the file is null
109         * @throws IOException in case of an I/O error
110         */
111        public LockableFileWriter(File file) throws IOException {
112            this(file, false, null);
113        }
114    
115        /**
116         * Constructs a LockableFileWriter.
117         *
118         * @param file  the file to write to, not null
119         * @param append  true if content should be appended, false to overwrite
120         * @throws NullPointerException if the file is null
121         * @throws IOException in case of an I/O error
122         */
123        public LockableFileWriter(File file, boolean append) throws IOException {
124            this(file, append, null);
125        }
126    
127        /**
128         * Constructs a LockableFileWriter.
129         *
130         * @param file  the file to write to, not null
131         * @param append  true if content should be appended, false to overwrite
132         * @param lockDir  the directory in which the lock file should be held
133         * @throws NullPointerException if the file is null
134         * @throws IOException in case of an I/O error
135         */
136        public LockableFileWriter(File file, boolean append, String lockDir) throws IOException {
137            this(file, Charset.defaultCharset(), append, lockDir);
138        }
139    
140        /**
141         * Constructs a LockableFileWriter with a file encoding.
142         *
143         * @param file  the file to write to, not null
144         * @param encoding  the encoding to use, null means platform default
145         * @throws NullPointerException if the file is null
146         * @throws IOException in case of an I/O error
147         * @since 2.3
148         */
149        public LockableFileWriter(File file, Charset encoding) throws IOException {
150            this(file, encoding, false, null);
151        }
152    
153        /**
154         * Constructs a LockableFileWriter with a file encoding.
155         *
156         * @param file  the file to write to, not null
157         * @param encoding  the encoding to use, null means platform default
158         * @throws NullPointerException if the file is null
159         * @throws IOException in case of an I/O error
160         * @throws UnsupportedCharsetException
161         *             thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
162         *             supported.
163         */
164        public LockableFileWriter(File file, String encoding) throws IOException {
165            this(file, encoding, false, null);
166        }
167    
168        /**
169         * Constructs a LockableFileWriter with a file encoding.
170         *
171         * @param file  the file to write to, not null
172         * @param encoding  the encoding to use, null means platform default
173         * @param append  true if content should be appended, false to overwrite
174         * @param lockDir  the directory in which the lock file should be held
175         * @throws NullPointerException if the file is null
176         * @throws IOException in case of an I/O error
177         * @since 2.3
178         */
179        public LockableFileWriter(File file, Charset encoding, boolean append,
180                String lockDir) throws IOException {
181            super();
182            // init file to create/append
183            file = file.getAbsoluteFile();
184            if (file.getParentFile() != null) {
185                FileUtils.forceMkdir(file.getParentFile());
186            }
187            if (file.isDirectory()) {
188                throw new IOException("File specified is a directory");
189            }
190            
191            // init lock file
192            if (lockDir == null) {
193                lockDir = System.getProperty("java.io.tmpdir");
194            }
195            File lockDirFile = new File(lockDir);
196            FileUtils.forceMkdir(lockDirFile);
197            testLockDir(lockDirFile);
198            lockFile = new File(lockDirFile, file.getName() + LCK);
199            
200            // check if locked
201            createLock();
202            
203            // init wrapped writer
204            out = initWriter(file, encoding, append);
205        }
206    
207        /**
208         * Constructs a LockableFileWriter with a file encoding.
209         *
210         * @param file  the file to write to, not null
211         * @param encoding  the encoding to use, null means platform default
212         * @param append  true if content should be appended, false to overwrite
213         * @param lockDir  the directory in which the lock file should be held
214         * @throws NullPointerException if the file is null
215         * @throws IOException in case of an I/O error
216         * @throws UnsupportedCharsetException
217         *             thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not
218         *             supported.
219         */
220        public LockableFileWriter(File file, String encoding, boolean append,
221                String lockDir) throws IOException {
222            this(file, Charsets.toCharset(encoding), append, lockDir);
223        }
224    
225        //-----------------------------------------------------------------------
226        /**
227         * Tests that we can write to the lock directory.
228         *
229         * @param lockDir  the File representing the lock directory
230         * @throws IOException if we cannot write to the lock directory
231         * @throws IOException if we cannot find the lock file
232         */
233        private void testLockDir(File lockDir) throws IOException {
234            if (!lockDir.exists()) {
235                throw new IOException(
236                        "Could not find lockDir: " + lockDir.getAbsolutePath());
237            }
238            if (!lockDir.canWrite()) {
239                throw new IOException(
240                        "Could not write to lockDir: " + lockDir.getAbsolutePath());
241            }
242        }
243    
244        /**
245         * Creates the lock file.
246         *
247         * @throws IOException if we cannot create the file
248         */
249        private void createLock() throws IOException {
250            synchronized (LockableFileWriter.class) {
251                if (!lockFile.createNewFile()) {
252                    throw new IOException("Can't write file, lock " +
253                            lockFile.getAbsolutePath() + " exists");
254                }
255                lockFile.deleteOnExit();
256            }
257        }
258    
259        /**
260         * Initialise the wrapped file writer.
261         * Ensure that a cleanup occurs if the writer creation fails.
262         *
263         * @param file  the file to be accessed
264         * @param encoding  the encoding to use
265         * @param append  true to append
266         * @return The initialised writer
267         * @throws IOException if an error occurs
268         */
269        private Writer initWriter(File file, Charset encoding, boolean append) throws IOException {
270            boolean fileExistedAlready = file.exists();
271            OutputStream stream = null;
272            Writer writer = null;
273            try {
274                stream = new FileOutputStream(file.getAbsolutePath(), append);
275                writer = new OutputStreamWriter(stream, Charsets.toCharset(encoding));
276            } catch (IOException ex) {
277                IOUtils.closeQuietly(writer);
278                IOUtils.closeQuietly(stream);
279                FileUtils.deleteQuietly(lockFile);
280                if (fileExistedAlready == false) {
281                    FileUtils.deleteQuietly(file);
282                }
283                throw ex;
284            } catch (RuntimeException ex) {
285                IOUtils.closeQuietly(writer);
286                IOUtils.closeQuietly(stream);
287                FileUtils.deleteQuietly(lockFile);
288                if (fileExistedAlready == false) {
289                    FileUtils.deleteQuietly(file);
290                }
291                throw ex;
292            }
293            return writer;
294        }
295    
296        //-----------------------------------------------------------------------
297        /**
298         * Closes the file writer and deletes the lockfile (if possible).
299         *
300         * @throws IOException if an I/O error occurs
301         */
302        @Override
303        public void close() throws IOException {
304            try {
305                out.close();
306            } finally {
307                lockFile.delete();
308            }
309        }
310    
311        //-----------------------------------------------------------------------
312        /**
313         * Write a character.
314         * @param idx the character to write
315         * @throws IOException if an I/O error occurs
316         */
317        @Override
318        public void write(int idx) throws IOException {
319            out.write(idx);
320        }
321    
322        /**
323         * Write the characters from an array.
324         * @param chr the characters to write
325         * @throws IOException if an I/O error occurs
326         */
327        @Override
328        public void write(char[] chr) throws IOException {
329            out.write(chr);
330        }
331    
332        /**
333         * Write the specified characters from an array.
334         * @param chr the characters to write
335         * @param st The start offset
336         * @param end The number of characters to write
337         * @throws IOException if an I/O error occurs
338         */
339        @Override
340        public void write(char[] chr, int st, int end) throws IOException {
341            out.write(chr, st, end);
342        }
343    
344        /**
345         * Write the characters from a string.
346         * @param str the string to write
347         * @throws IOException if an I/O error occurs
348         */
349        @Override
350        public void write(String str) throws IOException {
351            out.write(str);
352        }
353    
354        /**
355         * Write the specified characters from a string.
356         * @param str the string to write
357         * @param st The start offset
358         * @param end The number of characters to write
359         * @throws IOException if an I/O error occurs
360         */
361        @Override
362        public void write(String str, int st, int end) throws IOException {
363            out.write(str, st, end);
364        }
365    
366        /**
367         * Flush the stream.
368         * @throws IOException if an I/O error occurs
369         */
370        @Override
371        public void flush() throws IOException {
372            out.flush();
373        }
374    
375    }