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