All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.util.logging.handler.ArchivingFileHandler Maven / Gradle / Ivy

Go to download

A versatile Java(TM) library that implements many useful container and utility classes.

There is a newer version: 1.1.12
Show newest version

/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2012, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. The name of the author may not be used to endorse or promote products derived from this software without
 *       specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.util.logging.handler;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil.Produmer;
import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.expression.EvaluationException;
import de.unkrig.commons.text.parser.ParseException;
import de.unkrig.commons.util.TimeTable;
import de.unkrig.commons.util.logging.LogUtil;

/**
 * A log record handler which writes formatted records to an {@link OutputStream} which must be specified through
 * a ".outputStream" logging property.
 */
@NotNullByDefault(false) public
class ArchivingFileHandler extends AbstractStreamHandler {

    /**
     * No-arg constructor to be used by the log manager.
     */
    public
    ArchivingFileHandler() throws ParseException, EvaluationException, IOException {
        this(null);
    }

    /**
     * Single-arg constructor to be used by proxies.
     */
    public
    ArchivingFileHandler(@Nullable String propertyNamePrefix) throws ParseException, EvaluationException, IOException {
        super(propertyNamePrefix);

        if (propertyNamePrefix == null) propertyNamePrefix = this.getClass().getName();

        // We cannot be sure how the LogManager processes exceptions thrown by this constructor, so we print a stack
        // trace to STDERR before we rethrow the exception.
        // (The JRE default log manager prints a stack trace, too, so we'll see two.)
        try {
            this.init(
                LogUtil.getLoggingProperty(propertyNamePrefix + ".pattern",   ArchivingFileHandler.DEFAULT_PATTERN),
                LogUtil.getLoggingProperty(propertyNamePrefix + ".sizeLimit", ArchivingFileHandler.DEFAULT_SIZE_LIMIT),
                LogUtil.getLoggingProperty(
                    propertyNamePrefix + ".timeTable",
                    TimeTable.class,
                    ArchivingFileHandler.DEFAULT_TIME_TABLE
                ),
                LogUtil.getLoggingProperty(propertyNamePrefix + ".append", false)
            );
        } catch (ParseException pe) {
            pe.printStackTrace();
            throw pe;
        } catch (EvaluationException ee) {
            ee.printStackTrace();
            throw ee;
        } catch (RuntimeException re) {
            re.printStackTrace();
            throw re;
        }
    }

    /**
     * @param pattern   The pattern for the archive file names: '%d' is replaced with the current date
     * @param sizeLimit The size limit for the current file
     * @param timeTable The time table for time-based archiving
     * @param append    Whether to append to an existring current file
     * @param autoFlush See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
     * @param level     See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
     * @param filter    See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
     * @param formatter See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
     * @param encoding  See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
     */
    public
    ArchivingFileHandler(
        String    pattern,
        long      sizeLimit,
        TimeTable timeTable,
        boolean   append,
        boolean   autoFlush,
        Level     level,
        Filter    filter,
        Formatter formatter,
        String    encoding
    ) throws IOException {
        super(autoFlush, level, filter, formatter, encoding);
        this.init(pattern, sizeLimit, timeTable, append);
    }

    @Override public synchronized void
    publish(LogRecord record) {

        // Check the current time.
        if (new Date().compareTo(this.nextArchiving) >= 0) {
            try {
                this.archive(this.nextArchiving);
            } catch (Exception e) {
                this.reportError(null, e, ErrorManager.WRITE_FAILURE);
            }
        }

        super.publish(record);

        // Check the current file size.
        Long fileSize = this.byteCount.produce();
        assert fileSize != null;
        if (fileSize >= this.sizeLimit) {
            try {
                this.archive(new Date());
            } catch (Exception e) {
                this.reportError(null, e, ErrorManager.WRITE_FAILURE);
            }
        }
    }

    // CONFIGURATION

    /**
     * The pattern for the archive file names: Contains '%d', which must be replaced with the current date.
     */
    private String fileNamePattern;

    /**
     * The size limit for the current file.
     */
    private long sizeLimit;

    /**
     * The time table for time-based archiving.
     */
    private TimeTable timeTable;

    // STATE

    /**
     * Name of the "current file", i.e. the file that is written to, as opposed to the "archive files".
     */
    private File currentFile;

    /**
     * The point-in-time of the next time table-based archiving.
     */
    private Date nextArchiving;

    /**
     * Tracks the size of the current file.
     */
    private Produmer byteCount;

    // CONSTANTS

    /**
     * A special value for the {@code sizeLimit} paramter of {@link ArchivingFileHandler#ArchivingFileHandler(String,
     * long, TimeTable, boolean, boolean, Level, Filter, Formatter, String)} indicating that no limit should apply.
     */
    public static final long NO_LIMIT = Long.MAX_VALUE;

    /**
     * The {@link SimpleDateFormat} pattern for the '%d' variable.
     */
    private static final String DATE_PATTERN = "_yyyy-MM-dd_HH-mm-ss";

    // Default values for the configuration parameters:

    private static final String    DEFAULT_PATTERN    = "%h/java%d.log";
    private static final long      DEFAULT_SIZE_LIMIT = ArchivingFileHandler.NO_LIMIT;
    private static final TimeTable DEFAULT_TIME_TABLE = TimeTable.NEVER;

    // IMPLEMENTATION

    private void
    init(String pattern, long sizeLimit, TimeTable timeTable, boolean append)
    throws IOException {

        // Preprocess the file name pattern.
        pattern = pattern.replace('/', File.separatorChar);
        pattern = ArchivingFileHandler.replaceAll(pattern, "%%", "\u0001");
        pattern = ArchivingFileHandler.replaceAll(
            pattern,
            "%h",
            System.getProperty("user.home", ".") + File.separatorChar
        );
        pattern = ArchivingFileHandler.replaceAll(
            pattern,
            "%t",
            System.getProperty("java.io.tmpdir", ".") + File.separatorChar
        );
        if (!pattern.contains("%d")) pattern += "%d";
        this.fileNamePattern = (pattern = pattern.replace('\u0001', '%'));
        this.currentFile     = new File(ArchivingFileHandler.replaceAll(pattern, "%d", ""));

        // Remember size limit and time table.
        this.sizeLimit = sizeLimit;
        this.timeTable = timeTable;

        // Rename an existing "current file" if we're not in APPEND mode or the file is too large.
        if (this.currentFile.exists() && (!append || this.currentFile.length() >= this.sizeLimit)) {
            if (!this.currentFile.renameTo(this.getArchiveFile(new Date()))) {

                // For whatever reason, the current file could not be renamed. Most likely (under Windows) another
                // program (or logger) has the file open.
                // The fallback strategy is to forget about size limit, appending etc. and start appending to the
                // current log file.
                this.sizeLimit     = ArchivingFileHandler.NO_LIMIT;
                this.nextArchiving = TimeTable.MAX_DATE;
            }
        }

        this.openCurrentFile();
        this.nextArchiving = this.timeTable.next(new Date());
    }

    /**
     * @return A file that is guaranteed not to exist
     */
    private File
    getArchiveFile(Date date) {

        // Compute the archiv file name.
        String archiveFileName = ArchivingFileHandler.replaceAll(
            this.fileNamePattern,
            "%d",
            new SimpleDateFormat(ArchivingFileHandler.DATE_PATTERN).format(date)
        );
        File archiveFile = new File(archiveFileName);

        // If log files rotate very quickly, there might be a name clash. Append an integer number to the archive file
        // name to make it unique.
        if (archiveFile.exists()) {
            for (int uniqueId = 1;; uniqueId++) {
                File alternateArchiveFile = new File(archiveFileName + '.' + uniqueId);
                if (!alternateArchiveFile.exists()) {
                    archiveFile = alternateArchiveFile;
                    break;
                }
            }
        }

        return archiveFile;
    }

    private void
    openCurrentFile() throws IOException {

        this.byteCount = ConsumerUtil.store();

        this.setOutputStream(IoUtil.tee(
            new FileOutputStream(this.currentFile, true),
            IoUtil.lengthWritten(ConsumerUtil.cumulate(this.byteCount, 0L))
        ));
    }

    /**
     * Closes the current file, renames it, and recreates the current file.
     */
    private void
    archive(Date date) throws IOException {

        // Close the current file.
        this.close();

        // Archive (rename) the current file.
        if (!this.currentFile.renameTo(this.getArchiveFile(date))) {

            // For whatever reason, the current file could not be renamed. Most likely (under Windows) another program
            // (or logger) has the file open.
            // The fallback strategy is to forget about size limits, appending etc. and re-open the current log file.
            this.sizeLimit     = ArchivingFileHandler.NO_LIMIT;
            this.nextArchiving = TimeTable.MAX_DATE;
        } else {
            this.nextArchiving = this.timeTable.next(new Date());
        }

        // Re-open/re-create the current file.
        this.openCurrentFile();
    }

    /**
     * Replace all occurrences of {@code infix} within {@subject} with the given {@code replacement}.
     */
    private static String
    replaceAll(String subject, String infix, String replacement) {
        for (int idx = subject.indexOf(infix); idx != -1; idx = subject.indexOf(infix, idx + replacement.length())) {
            subject = subject.substring(0, idx) + replacement + subject.substring(idx + infix.length());
        }
        return subject;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy