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 */
17
18package java.util.logging;
19
20import java.io.BufferedOutputStream;
21import java.io.File;
22import java.io.FileNotFoundException;
23import java.io.FileOutputStream;
24import java.io.IOException;
25import java.io.OutputStream;
26import java.nio.channels.FileChannel;
27import java.nio.channels.FileLock;
28import java.util.Hashtable;
29import libcore.io.IoUtils;
30
31/**
32 * A {@code FileHandler} writes logging records into a specified file or a
33 * rotating set of files.
34 * <p>
35 * When a set of files is used and a given amount of data has been written to
36 * one file, then this file is closed and another file is opened. The name of
37 * these files are generated by given name pattern, see below for details.
38 * When the files have all been filled the Handler returns to the first and goes
39 * through the set again.
40 * <p>
41 * By default, the I/O buffering mechanism is enabled, but when each log record
42 * is complete, it is flushed out.
43 * <p>
44 * {@code XMLFormatter} is the default formatter for {@code FileHandler}.
45 * <p>
46 * {@code FileHandler} reads the following {@code LogManager} properties for
47 * initialization; if a property is not defined or has an invalid value, a
48 * default value is used.
49 * <ul>
50 * <li>java.util.logging.FileHandler.append specifies whether this
51 * {@code FileHandler} should append onto existing files, defaults to
52 * {@code false}.</li>
53 * <li>java.util.logging.FileHandler.count specifies how many output files to
54 * rotate, defaults to 1.</li>
55 * <li>java.util.logging.FileHandler.filter specifies the {@code Filter} class
56 * name, defaults to no {@code Filter}.</li>
57 * <li>java.util.logging.FileHandler.formatter specifies the {@code Formatter}
58 * class, defaults to {@code java.util.logging.XMLFormatter}.</li>
59 * <li>java.util.logging.FileHandler.encoding specifies the character set
60 * encoding name, defaults to the default platform encoding.</li>
61 * <li>java.util.logging.FileHandler.level specifies the level for this
62 * {@code Handler}, defaults to {@code Level.ALL}.</li>
63 * <li>java.util.logging.FileHandler.limit specifies the maximum number of
64 * bytes to write to any one file, defaults to zero, which means no limit.</li>
65 * <li>java.util.logging.FileHandler.pattern specifies name pattern for the
66 * output files. See below for details. Defaults to "%h/java%u.log".</li>
67 * </ul>
68 * <p>
69 * Name pattern is a string that may include some special substrings, which will
70 * be replaced to generate output files:
71 * <ul>
72 * <li>"/" represents the local pathname separator</li>
73 * <li>"%g" represents the generation number to distinguish rotated logs</li>
74 * <li>"%h" represents the home directory of the current user, which is
75 * specified by "user.home" system property</li>
76 * <li>"%t" represents the system's temporary directory</li>
77 * <li>"%u" represents a unique number to resolve conflicts</li>
78 * <li>"%%" represents the percent sign character '%'</li>
79 * </ul>
80 * <p>
81 * Normally, the generation numbers are not larger than the given file count and
82 * follow the sequence 0, 1, 2.... If the file count is larger than one, but the
83 * generation field("%g") has not been specified in the pattern, then the
84 * generation number after a dot will be added to the end of the file name.
85 * <p>
86 * The "%u" unique field is used to avoid conflicts and is set to 0 at first. If
87 * one {@code FileHandler} tries to open the filename which is currently in use
88 * by another process, it will repeatedly increment the unique number field and
89 * try again. If the "%u" component has not been included in the file name
90 * pattern and some contention on a file does occur, then a unique numerical
91 * value will be added to the end of the filename in question immediately to the
92 * right of a dot. The generation of unique IDs for avoiding conflicts is only
93 * guaranteed to work reliably when using a local disk file system.
94 */
95public class FileHandler extends StreamHandler {
96
97    private static final String LCK_EXT = ".lck";
98
99    private static final int DEFAULT_COUNT = 1;
100
101    private static final int DEFAULT_LIMIT = 0;
102
103    private static final boolean DEFAULT_APPEND = false;
104
105    private static final String DEFAULT_PATTERN = "%h/java%u.log";
106
107    // maintain all file locks hold by this process
108    private static final Hashtable<String, FileLock> allLocks = new Hashtable<String, FileLock>();
109
110    // the count of files which the output cycle through
111    private int count;
112
113    // the size limitation in byte of log file
114    private int limit;
115
116    // whether the FileHandler should open a existing file for output in append
117    // mode
118    private boolean append;
119
120    // the pattern for output file name
121    private String pattern;
122
123    // maintain a LogManager instance for convenience
124    private LogManager manager;
125
126    // output stream, which can measure the output file length
127    private MeasureOutputStream output;
128
129    // used output file
130    private File[] files;
131
132    // output file lock
133    FileLock lock = null;
134
135    // current output file name
136    String fileName = null;
137
138    // current unique ID
139    int uniqueID = -1;
140
141    /**
142     * Construct a {@code FileHandler} using {@code LogManager} properties or
143     * their default value.
144     *
145     * @throws IOException
146     *             if any I/O error occurs.
147     */
148    public FileHandler() throws IOException {
149        init(null, null, null, null);
150    }
151
152    // init properties
153    private void init(String p, Boolean a, Integer l, Integer c)
154            throws IOException {
155        // check access
156        manager = LogManager.getLogManager();
157        manager.checkAccess();
158        initProperties(p, a, l, c);
159        initOutputFiles();
160    }
161
162    private void initOutputFiles() throws FileNotFoundException, IOException {
163        while (true) {
164            // try to find a unique file which is not locked by other process
165            uniqueID++;
166            // FIXME: improve performance here
167            for (int generation = 0; generation < count; generation++) {
168                // cache all file names for rotation use
169                files[generation] = new File(parseFileName(generation));
170            }
171            fileName = files[0].getAbsolutePath();
172            synchronized (allLocks) {
173                /*
174                 * if current process has held lock for this fileName continue
175                 * to find next file
176                 */
177                if (allLocks.get(fileName) != null) {
178                    continue;
179                }
180                if (files[0].exists()
181                        && (!append || files[0].length() >= limit)) {
182                    for (int i = count - 1; i > 0; i--) {
183                        if (files[i].exists()) {
184                            files[i].delete();
185                        }
186                        files[i - 1].renameTo(files[i]);
187                    }
188                }
189                FileOutputStream fileStream = new FileOutputStream(fileName
190                        + LCK_EXT);
191                FileChannel channel = fileStream.getChannel();
192                /*
193                 * if lock is unsupported and IOException thrown, just let the
194                 * IOException throws out and exit otherwise it will go into an
195                 * undead cycle
196                 */
197                lock = channel.tryLock();
198                if (lock == null) {
199                    IoUtils.closeQuietly(fileStream);
200                    continue;
201                }
202                allLocks.put(fileName, lock);
203                break;
204            }
205        }
206        output = new MeasureOutputStream(new BufferedOutputStream(
207                new FileOutputStream(fileName, append)), files[0].length());
208        setOutputStream(output);
209    }
210
211    private void initProperties(String p, Boolean a, Integer l, Integer c) {
212        super.initProperties("ALL", null, "java.util.logging.XMLFormatter",
213                null);
214        String className = this.getClass().getName();
215        pattern = (p == null) ? getStringProperty(className + ".pattern",
216                DEFAULT_PATTERN) : p;
217        if (pattern == null) {
218            throw new NullPointerException("pattern == null");
219        } else if (pattern.isEmpty()) {
220            throw new NullPointerException("pattern.isEmpty()");
221        }
222        append = (a == null) ? getBooleanProperty(className + ".append",
223                DEFAULT_APPEND) : a.booleanValue();
224        count = (c == null) ? getIntProperty(className + ".count",
225                DEFAULT_COUNT) : c.intValue();
226        limit = (l == null) ? getIntProperty(className + ".limit",
227                DEFAULT_LIMIT) : l.intValue();
228        count = count < 1 ? DEFAULT_COUNT : count;
229        limit = limit < 0 ? DEFAULT_LIMIT : limit;
230        files = new File[count];
231    }
232
233    void findNextGeneration() {
234        super.close();
235        for (int i = count - 1; i > 0; i--) {
236            if (files[i].exists()) {
237                files[i].delete();
238            }
239            files[i - 1].renameTo(files[i]);
240        }
241        try {
242            output = new MeasureOutputStream(new BufferedOutputStream(
243                    new FileOutputStream(files[0])));
244        } catch (FileNotFoundException e1) {
245            this.getErrorManager().error("Error opening log file", e1, ErrorManager.OPEN_FAILURE);
246        }
247        setOutputStream(output);
248    }
249
250    /**
251     * Transform the pattern to the valid file name, replacing any patterns, and
252     * applying generation and uniqueID if present.
253     *
254     * @param gen
255     *            generation of this file
256     * @return transformed filename ready for use.
257     */
258    private String parseFileName(int gen) {
259        int cur = 0;
260        int next = 0;
261        boolean hasUniqueID = false;
262        boolean hasGeneration = false;
263
264        // TODO privilege code?
265
266        String tempPath = System.getProperty("java.io.tmpdir");
267        boolean tempPathHasSepEnd = (tempPath == null ? false : tempPath
268                .endsWith(File.separator));
269
270        String homePath = System.getProperty("user.home");
271        boolean homePathHasSepEnd = (homePath == null ? false : homePath
272                .endsWith(File.separator));
273
274        StringBuilder sb = new StringBuilder();
275        pattern = pattern.replace('/', File.separatorChar);
276
277        char[] value = pattern.toCharArray();
278        while ((next = pattern.indexOf('%', cur)) >= 0) {
279            if (++next < pattern.length()) {
280                switch (value[next]) {
281                    case 'g':
282                        sb.append(value, cur, next - cur - 1).append(gen);
283                        hasGeneration = true;
284                        break;
285                    case 'u':
286                        sb.append(value, cur, next - cur - 1).append(uniqueID);
287                        hasUniqueID = true;
288                        break;
289                    case 't':
290                        /*
291                         * we should probably try to do something cute here like
292                         * lookahead for adjacent '/'
293                         */
294                        sb.append(value, cur, next - cur - 1).append(tempPath);
295                        if (!tempPathHasSepEnd) {
296                            sb.append(File.separator);
297                        }
298                        break;
299                    case 'h':
300                        sb.append(value, cur, next - cur - 1).append(homePath);
301                        if (!homePathHasSepEnd) {
302                            sb.append(File.separator);
303                        }
304                        break;
305                    case '%':
306                        sb.append(value, cur, next - cur - 1).append('%');
307                        break;
308                    default:
309                        sb.append(value, cur, next - cur);
310                }
311                cur = ++next;
312            } else {
313                // fail silently
314            }
315        }
316
317        sb.append(value, cur, value.length - cur);
318
319        if (!hasGeneration && count > 1) {
320            sb.append(".").append(gen);
321        }
322
323        if (!hasUniqueID && uniqueID > 0) {
324            sb.append(".").append(uniqueID);
325        }
326
327        return sb.toString();
328    }
329
330    // get boolean LogManager property, if invalid value got, using default
331    // value
332    private boolean getBooleanProperty(String key, boolean defaultValue) {
333        String property = manager.getProperty(key);
334        if (property == null) {
335            return defaultValue;
336        }
337        boolean result = defaultValue;
338        if ("true".equalsIgnoreCase(property)) {
339            result = true;
340        } else if ("false".equalsIgnoreCase(property)) {
341            result = false;
342        }
343        return result;
344    }
345
346    // get String LogManager property, if invalid value got, using default value
347    private String getStringProperty(String key, String defaultValue) {
348        String property = manager.getProperty(key);
349        return property == null ? defaultValue : property;
350    }
351
352    // get int LogManager property, if invalid value got, using default value
353    private int getIntProperty(String key, int defaultValue) {
354        String property = manager.getProperty(key);
355        int result = defaultValue;
356        if (property != null) {
357            try {
358                result = Integer.parseInt(property);
359            } catch (Exception e) {
360                // ignore
361            }
362        }
363        return result;
364    }
365
366    /**
367     * Constructs a new {@code FileHandler}. The given name pattern is used as
368     * output filename, the file limit is set to zero (no limit), the file count
369     * is set to one; the remaining configuration is done using
370     * {@code LogManager} properties or their default values. This handler
371     * writes to only one file with no size limit.
372     *
373     * @param pattern
374     *            the name pattern for the output file.
375     * @throws IOException
376     *             if any I/O error occurs.
377     * @throws IllegalArgumentException
378     *             if the pattern is empty.
379     * @throws NullPointerException
380     *             if the pattern is {@code null}.
381     */
382    public FileHandler(String pattern) throws IOException {
383        if (pattern.isEmpty()) {
384            throw new IllegalArgumentException("Pattern cannot be empty");
385        }
386        init(pattern, null, Integer.valueOf(DEFAULT_LIMIT), Integer.valueOf(DEFAULT_COUNT));
387    }
388
389    /**
390     * Construct a new {@code FileHandler}. The given name pattern is used as
391     * output filename, the file limit is set to zero (no limit), the file count
392     * is initialized to one and the value of {@code append} becomes the new
393     * instance's append mode. The remaining configuration is done using
394     * {@code LogManager} properties. This handler writes to only one file
395     * with no size limit.
396     *
397     * @param pattern
398     *            the name pattern for the output file.
399     * @param append
400     *            the append mode.
401     * @throws IOException
402     *             if any I/O error occurs.
403     * @throws IllegalArgumentException
404     *             if {@code pattern} is empty.
405     * @throws NullPointerException
406     *             if {@code pattern} is {@code null}.
407     */
408    public FileHandler(String pattern, boolean append) throws IOException {
409        if (pattern.isEmpty()) {
410            throw new IllegalArgumentException("Pattern cannot be empty");
411        }
412        init(pattern, Boolean.valueOf(append), Integer.valueOf(DEFAULT_LIMIT),
413                Integer.valueOf(DEFAULT_COUNT));
414    }
415
416    /**
417     * Construct a new {@code FileHandler}. The given name pattern is used as
418     * output filename, the maximum file size is set to {@code limit} and the
419     * file count is initialized to {@code count}. The remaining configuration
420     * is done using {@code LogManager} properties. This handler is configured
421     * to write to a rotating set of count files, when the limit of bytes has
422     * been written to one output file, another file will be opened instead.
423     *
424     * @param pattern
425     *            the name pattern for the output file.
426     * @param limit
427     *            the data amount limit in bytes of one output file, can not be
428     *            negative.
429     * @param count
430     *            the maximum number of files to use, can not be less than one.
431     * @throws IOException
432     *             if any I/O error occurs.
433     * @throws IllegalArgumentException
434     *             if {@code pattern} is empty, {@code limit < 0} or
435     *             {@code count < 1}.
436     * @throws NullPointerException
437     *             if {@code pattern} is {@code null}.
438     */
439    public FileHandler(String pattern, int limit, int count) throws IOException {
440        if (pattern.isEmpty()) {
441            throw new IllegalArgumentException("Pattern cannot be empty");
442        }
443        if (limit < 0 || count < 1) {
444            throw new IllegalArgumentException("limit < 0 || count < 1");
445        }
446        init(pattern, null, Integer.valueOf(limit), Integer.valueOf(count));
447    }
448
449    /**
450     * Construct a new {@code FileHandler}. The given name pattern is used as
451     * output filename, the maximum file size is set to {@code limit}, the file
452     * count is initialized to {@code count} and the append mode is set to
453     * {@code append}. The remaining configuration is done using
454     * {@code LogManager} properties. This handler is configured to write to a
455     * rotating set of count files, when the limit of bytes has been written to
456     * one output file, another file will be opened instead.
457     *
458     * @param pattern
459     *            the name pattern for the output file.
460     * @param limit
461     *            the data amount limit in bytes of one output file, can not be
462     *            negative.
463     * @param count
464     *            the maximum number of files to use, can not be less than one.
465     * @param append
466     *            the append mode.
467     * @throws IOException
468     *             if any I/O error occurs.
469     * @throws IllegalArgumentException
470     *             if {@code pattern} is empty, {@code limit < 0} or
471     *             {@code count < 1}.
472     * @throws NullPointerException
473     *             if {@code pattern} is {@code null}.
474     */
475    public FileHandler(String pattern, int limit, int count, boolean append) throws IOException {
476        if (pattern.isEmpty()) {
477            throw new IllegalArgumentException("Pattern cannot be empty");
478        }
479        if (limit < 0 || count < 1) {
480            throw new IllegalArgumentException("limit < 0 || count < 1");
481        }
482        init(pattern, Boolean.valueOf(append), Integer.valueOf(limit), Integer.valueOf(count));
483    }
484
485    /**
486     * Flushes and closes all opened files.
487     */
488    @Override
489    public void close() {
490        // release locks
491        super.close();
492        allLocks.remove(fileName);
493        try {
494            FileChannel channel = lock.channel();
495            lock.release();
496            channel.close();
497            File file = new File(fileName + LCK_EXT);
498            file.delete();
499        } catch (IOException e) {
500            // ignore
501        }
502    }
503
504    /**
505     * Publish a {@code LogRecord}.
506     *
507     * @param record
508     *            the log record to publish.
509     */
510    @Override
511    public synchronized void publish(LogRecord record) {
512        super.publish(record);
513        flush();
514        if (limit > 0 && output.getLength() >= limit) {
515            findNextGeneration();
516        }
517    }
518
519    /**
520     * This output stream uses the decorator pattern to add measurement features
521     * to OutputStream which can detect the total size(in bytes) of output, the
522     * initial size can be set.
523     */
524    static class MeasureOutputStream extends OutputStream {
525
526        OutputStream wrapped;
527
528        long length;
529
530        public MeasureOutputStream(OutputStream stream, long currentLength) {
531            wrapped = stream;
532            length = currentLength;
533        }
534
535        public MeasureOutputStream(OutputStream stream) {
536            this(stream, 0);
537        }
538
539        @Override
540        public void write(int oneByte) throws IOException {
541            wrapped.write(oneByte);
542            length++;
543        }
544
545        @Override
546        public void write(byte[] b, int off, int len) throws IOException {
547            wrapped.write(b, off, len);
548            length += len;
549        }
550
551        @Override
552        public void close() throws IOException {
553            wrapped.close();
554        }
555
556        @Override
557        public void flush() throws IOException {
558            wrapped.flush();
559        }
560
561        public long getLength() {
562            return length;
563        }
564
565        public void setLength(long newLength) {
566            length = newLength;
567        }
568    }
569}
570