/*
 * 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.
     * 
     * 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;
    }
}
