WriterAppender.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.QuietWriter;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.LoggingEvent;

// Contibutors: Jens Uwe Pipka <jens.pipka@gmx.de>
//              Ben Sandee

/**
 * WriterAppender appends log events to a {@link java.io.Writer} or an {@link java.io.OutputStream} depending on the
 * user's choice.
 *
 * @author Ceki G&uuml;lc&uuml;
 * @since 1.1
 */
public class WriterAppender extends AppenderSkeleton {

    /**
     * Immediate flush means that the underlying writer or output stream will be flushed at the end of each append
     * operation unless shouldFlush() is overridden. Immediate flush is slower but ensures that each append request is
     * actually written. If <code>immediateFlush</code> is set to
     * <code>false</code>, then there is a good chance that the last few logs events
     * are not actually written to persistent media if and when the application crashes.
     *
     * <p>
     * The <code>immediateFlush</code> variable is set to <code>true</code> by default.
     */
    protected boolean immediateFlush = true;

    /**
     * The encoding to use when writing.
     * <p>
     * The <code>encoding</code> variable is set to <code>null</null> by default which results in the utilization of the
     * system's default encoding.
     */
    protected String encoding;

    /**
     * This is the {@link QuietWriter quietWriter} where we will write to.
     */
    protected QuietWriter qw;

    /**
     * This default constructor does nothing.
     */
    public WriterAppender() {
    }

    /**
     * Instantiate a WriterAppender and set the output destination to a new {@link OutputStreamWriter} initialized with
     * <code>os</code> as its {@link OutputStream}.
     */
    public WriterAppender(Layout layout, OutputStream os) {
        this(layout, new OutputStreamWriter(os));
    }

    /**
     * Instantiate a WriterAppender and set the output destination to
     * <code>writer</code>.
     *
     * <p>
     * The <code>writer</code> must have been previously opened by the user.
     */
    public WriterAppender(Layout layout, Writer writer) {
        this.layout = layout;
        this.setWriter(writer);
    }

    /**
     * If the <b>ImmediateFlush</b> option is set to <code>true</code>, the appender will flush at the end of each
     * write. This is the default behavior. If the option is set to <code>false</code>, then the underlying stream can
     * defer writing to physical medium to a later time.
     *
     * <p>
     * Avoiding the flush operation at the end of each append results in a performance gain of 10 to 20 percent.
     * However, there is safety tradeoff involved in skipping flushing. Indeed, when flushing is skipped, then it is
     * likely that the last few log events will not be recorded on disk when the application exits. This is a high price
     * to pay even for a 20% performance gain.
     */
    public void setImmediateFlush(boolean value) {
        immediateFlush = value;
    }

    /**
     * Returns value of the <b>ImmediateFlush</b> option.
     */
    public boolean getImmediateFlush() {
        return immediateFlush;
    }

    /**
     * Does nothing.
     */
    public void activateOptions() {
    }

    /**
     * This method is called by the {@link AppenderSkeleton#doAppend} method.
     *
     * <p>
     * If the output stream exists and is writable then write a log statement to the output stream. Otherwise, write a
     * single warning message to
     * <code>System.err</code>.
     *
     * <p>
     * The format of the output will depend on this appender's layout.
     */
    public void append(LoggingEvent event) {

        // Reminder: the nesting of calls is:
        //
        // doAppend()
        // - check threshold
        // - filter
        // - append();
        // - checkEntryConditions();
        // - subAppend();

        if (!checkEntryConditions()) {
            return;
        }
        subAppend(event);
    }

    /**
     * This method determines if there is a sense in attempting to append.
     *
     * <p>
     * It checks whether there is a set output target and also if there is a set layout. If these checks fail, then the
     * boolean value <code>false</code> is returned.
     */
    protected boolean checkEntryConditions() {
        if (this.closed) {
            LogLog.warn("Not allowed to write to a closed appender.");
            return false;
        }

        if (this.qw == null) {
            errorHandler.error("No output stream or file set for the appender named [" + name + "].");
            return false;
        }

        if (this.layout == null) {
            errorHandler.error("No layout set for the appender named [" + name + "].");
            return false;
        }
        return true;
    }

    /**
     * Close this appender instance. The underlying stream or writer is also closed.
     *
     * <p>
     * Closed appenders cannot be reused.
     *
     * @see #setWriter
     * @since 0.8.4
     */
    public synchronized void close() {
        if (this.closed)
            return;
        this.closed = true;
        writeFooter();
        reset();
    }

    /**
     * Close the underlying {@link java.io.Writer}.
     */
    protected void closeWriter() {
        if (qw != null) {
            try {
                qw.close();
            } catch (IOException e) {
                if (e instanceof InterruptedIOException) {
                    Thread.currentThread().interrupt();
                }
                // There is do need to invoke an error handler at this late
                // stage.
                LogLog.error("Could not close " + qw, e);
            }
        }
    }

    /**
     * Returns an OutputStreamWriter when passed an OutputStream. The encoding used will depend on the value of the
     * <code>encoding</code> property. If the encoding value is specified incorrectly the writer will be opened using
     * the default system encoding (an error message will be printed to the loglog.
     */
    protected OutputStreamWriter createWriter(OutputStream os) {
        OutputStreamWriter retval = null;

        String enc = getEncoding();
        if (enc != null) {
            try {
                retval = new OutputStreamWriter(os, enc);
            } catch (IOException e) {
                if (e instanceof InterruptedIOException) {
                    Thread.currentThread().interrupt();
                }
                LogLog.warn("Error initializing output writer.");
                LogLog.warn("Unsupported encoding?");
            }
        }
        if (retval == null) {
            retval = new OutputStreamWriter(os);
        }
        return retval;
    }

    public String getEncoding() {
        return encoding;
    }

    public void setEncoding(String value) {
        encoding = value;
    }

    /**
     * Set the {@link ErrorHandler} for this WriterAppender and also the underlying {@link QuietWriter} if any.
     */
    public synchronized void setErrorHandler(ErrorHandler eh) {
        if (eh == null) {
            LogLog.warn("You have tried to set a null error-handler.");
        } else {
            this.errorHandler = eh;
            if (this.qw != null) {
                this.qw.setErrorHandler(eh);
            }
        }
    }

    /**
     * <p>
     * Sets the Writer where the log output will go. The specified Writer must be opened by the user and be writable.
     *
     * <p>
     * The <code>java.io.Writer</code> will be closed when the appender instance is closed.
     *
     *
     * <p>
     * <b>WARNING:</b> Logging to an unopened Writer will fail.
     * <p>
     *
     * @param writer An already opened Writer.
     */
    public synchronized void setWriter(Writer writer) {
        reset();
        this.qw = new QuietWriter(writer, errorHandler);
        // this.tp = new TracerPrintWriter(qw);
        writeHeader();
    }

    /**
     * Actual writing occurs here.
     *
     * <p>
     * Most subclasses of <code>WriterAppender</code> will need to override this method.
     *
     * @since 0.9.0
     */
    protected void subAppend(LoggingEvent event) {
        this.qw.write(this.layout.format(event));

        if (layout.ignoresThrowable()) {
            String[] s = event.getThrowableStrRep();
            if (s != null) {
                int len = s.length;
                for (int i = 0; i < len; i++) {
                    this.qw.write(s[i]);
                    this.qw.write(Layout.LINE_SEP);
                }
            }
        }

        if (shouldFlush(event)) {
            this.qw.flush();
        }
    }

    /**
     * The WriterAppender requires a layout. Hence, this method returns
     * <code>true</code>.
     */
    public boolean requiresLayout() {
        return true;
    }

    /**
     * Clear internal references to the writer and other variables.
     * <p>
     * Subclasses can override this method for an alternate closing behavior.
     */
    protected void reset() {
        closeWriter();
        this.qw = null;
        // this.tp = null;
    }

    /**
     * Write a footer as produced by the embedded layout's {@link Layout#getFooter} method.
     */
    protected void writeFooter() {
        if (layout != null) {
            String f = layout.getFooter();
            if (f != null && this.qw != null) {
                this.qw.write(f);
                this.qw.flush();
            }
        }
    }

    /**
     * Write a header as produced by the embedded layout's {@link Layout#getHeader} method.
     */
    protected void writeHeader() {
        if (layout != null) {
            String h = layout.getHeader();
            if (h != null && this.qw != null)
                this.qw.write(h);
        }
    }

    /**
     * Determines whether the writer should be flushed after this event is written.
     *
     * @since 1.2.16
     */
    protected boolean shouldFlush(final LoggingEvent event) {
        return immediateFlush;
    }
}