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 */
17package org.apache.commons.io.output;
18
19import java.io.File;
20import java.io.FileOutputStream;
21import java.io.FileWriter;
22import java.io.IOException;
23import java.io.OutputStream;
24import java.io.OutputStreamWriter;
25import java.io.Writer;
26
27import org.apache.commons.io.FileUtils;
28import 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 */
50public 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     */
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     */
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     */
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     */
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            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     */
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     */
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     */
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     */
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     */
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     */
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     */
329    public void flush() throws IOException {
330        out.flush();
331    }
332
333}
334