package com.trgs;

/* 
 * Copyright 1999,2004 The Apache Software Foundation.
 * 
 * Licensed 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.
 */

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Layout;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.QuietWriter;
import org.apache.log4j.spi.ErrorHandler;
import org.apache.log4j.spi.LoggingEvent;

/**
 * VariableNameAppender extends {@link AppenderSkeleton } so that the underlying
 * file is rolled over at a user chosen frequency.
 * 
 * <p>
 * The variable name is specified by the <b>File</b> option. This value should
 * follow the {@link PatternLayout} conventions. In particular, you
 * <em>must</em> escape literal text within a pair of single quotes. A formatted
 * version of the fileName pattern is used as the file name.
 * 
 * <p>
 * For example, if the <b>File</b> option is set to
 * <code>/log/server[%c].log</code> the file
 * <code>/foo/bar[com.foo.ClassName].log</code> will be generated to
 * com.foo.ClassName class and the file
 * <code>/foo/bar[com.foo.AnotherClassName].log</code> will be generated to
 * com.foo.AnotherClassName class.
 * 
 * <p>
 * It is possible to specify any pattern available in the {@link PatternLayout}
 * class.
 * 
 * <p>
 * If you specify a data parameter the VariableNameFileAppender will behave like
 * a RollingAppender creating a new file depending on the provided parameter
 * <p>
 * For example /foo/log[%d{dd-MMM-yyyy}].log will create a new file every day.
 * 
 * 
 * <p>
 * Is is possible to specify monthly, weekly, half-daily, daily, hourly, or
 * minutely rollover schedules.
 * 
 * 
 * <p>
 * Do not use the colon ":" character in anywhere in the <b>DatePattern</b>
 * option. The text before the colon is interpreted as the protocol
 * specification of a URL which is probably not what you want.
 * 
 * @author Eduardo Rodrigues
 */

public class ConfigurableFileNameAppender extends AppenderSkeleton {

	private PatternLayout fileNamePatternLayout;

	private Map<String, QuietWriter> qwMap = new HashMap<String, QuietWriter>();
	/**
	 * Immediate flush means that the underlying writer or output stream will be
	 * flushed at the end of each append operation. 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;
	/**
	 * Controls file truncation. The default value for this variable is
	 * <code>true</code>, meaning that by default a <code>FileAppender</code>
	 * will append to an existing file and not truncate it.
	 * 
	 * <p>
	 * This option is meaningful only if the FileAppender opens the file.
	 */
	protected boolean fileAppend = true;

	/**
	 * Do we do bufferedIO?
	 */
	protected boolean bufferedIO = false;

	/**
	 * Determines the size of IO buffer be. Default is 8K.
	 */
	protected int bufferSize = 8 * 1024;

	private String fileName;

	public ConfigurableFileNameAppender() {
		super();
	}

	public ConfigurableFileNameAppender(Layout layout, String filename)
			throws IOException {
		super();
		this.setFile(filename);
		this.layout = layout;
	}

	public ConfigurableFileNameAppender(Layout layout, String filename,
			boolean append) throws IOException {
		this(layout, filename);
		this.fileAppend = append;
	}

	public ConfigurableFileNameAppender(Layout layout, String filename,
			boolean append, boolean bufferedIO, int bufferSize)
			throws IOException {
		this(layout, filename, append);
		this.bufferedIO = bufferedIO;
		this.bufferSize = bufferSize;
	}

	public void setFile(String file) {
		this.fileName = file;
		fileNamePatternLayout = new PatternLayout(fileName);
	}

	/**
	 * If the value of <b>File</b> is not <code>null</code>, then
	 * {@link #setFile} is called with the values of <b>File</b> and
	 * <b>Append</b> properties.
	 * 
	 * @since 0.8.1
	 */
	public void activateOptions() {
		if (fileName == null) {
			LogLog.warn("FileName not set for appender [" + name + "].");
		}
	}

	/**
	 * this method can be overridden by the subclasses to improve the names
	 * 
	 * @param event
	 *            event to be logged.
	 * 
	 *            Returns the file name following the pattern provided.
	 * 
	 * @since 0.1
	 */
	protected String generateFileName(LoggingEvent event) {
		return fileNamePatternLayout.format(event);
	}

	protected void subAppend(LoggingEvent event) {
		// overrides super behavior creative a file depending on the file
		// pattern

		String currentFileName = generateFileName(event);
		QuietWriter qw = qwMap.get(currentFileName);
		if (qw == null) {
			try {
				qw = createFile(currentFileName, fileAppend, bufferedIO,
						bufferSize);
			} catch (IOException e) {
				errorHandler.error("createFile(" + currentFileName + ", "
						+ fileAppend + ") call failed.");
			}
			qwMap.put(currentFileName, qw);
		}
		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++) {
					qw.write(s[i]);
					qw.write(Layout.LINE_SEP);
				}
			}
		}

		if (this.immediateFlush) {
			qw.flush();
		}

	}

	public synchronized void setFile(String fileName, boolean append,
			boolean bufferedIO, int bufferSize) throws IOException {
	}

	/**
	 * <p>
	 * Sets and <i>opens</i> the file where the log output will go. The
	 * specified file must be writable.
	 * 
	 * <p>
	 * If there was already an opened file, then the previous file is closed
	 * first.
	 * 
	 * <p>
	 * <b>Do not use this method directly. To configure a FileAppender or one of
	 * its subclasses, set its properties one by one and then call
	 * activateOptions.</b>
	 * 
	 * @param fileName
	 *            The path to the log file.
	 * @param append
	 *            If true will append to fileName. Otherwise will truncate
	 *            fileName.
	 */
	public synchronized QuietWriter createFile(String fileName, boolean append,
			boolean bufferedIO, int bufferSize) throws IOException {
		LogLog.debug("setFile called: " + fileName + ", " + append);

		// It does not make sense to have immediate flush and bufferedIO.
		if (bufferedIO) {
			setImmediateFlush(false);
		}

		FileOutputStream ostream = null;
		try {
			//
			// attempt to create file
			//
			ostream = new FileOutputStream(fileName, append);
		} catch (FileNotFoundException ex) {
			//
			// if parent directory does not exist then
			// attempt to create it and try to create file
			// see bug 9150
			//
			String parentName = new File(fileName).getParent();
			if (parentName != null) {
				File parentDir = new File(parentName);
				if (!parentDir.exists() && parentDir.mkdirs()) {
					ostream = new FileOutputStream(fileName, append);
				} else {
					throw ex;
				}
			} else {
				throw ex;
			}
		}
		Writer fw = createWriter(ostream);
		if (bufferedIO) {
			fw = new BufferedWriter(fw, bufferSize);
		}
		QuietWriter qw = new QuietWriter(fw, errorHandler);
		writeHeader(qw);
		LogLog.debug("setFile ended");
		return qw;

	}

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

	/**
	 * Sets the quiet writer being used.
	 * 
	 * This method is overriden by {@link RollingFileAppender}.
	 */
	protected void setQWForFiles(Writer writer) {
		// this.qw = new QuietWriter(writer, errorHandler);
	}

	/**
	 * Clear internal references to the writer and other variables.
	 * 
	 * Subclasses can override this method for an alternate closing behavior.
	 */
	protected void reset() {
		closeWriter();
		this.qwMap = new HashMap<String, QuietWriter>();
	}

	/**
	 * Close the underlying {@link java.io.Writer}.
	 * */
	protected void closeWriter() {
		if (layout != null) {
			for (QuietWriter writer : qwMap.values()) {
				if (writer != null) {
					try {
						writer.close();
					} catch (IOException e) {
						// There is do need to invoke an error handler at
						// this late
						// stage.
						LogLog.error("Could not close " + writer, e);
					}
				}
			}
		}

	}

	/**
	 * Write a footer as produced by the embedded layout's
	 * {@link Layout#getFooter} method.
	 */
	protected void writeFooter() {
		if (layout != null) {
			String h = layout.getFooter();
			if (h != null) {
				for (QuietWriter writer : qwMap.values()) {
					if (writer != null) {
						writer.write(h);
						writer.flush();
					}
				}
			}
		}
	}

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

	/**
	 * 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;
			for (QuietWriter writer : qwMap.values()) {
				if (writer != null) {
					writer.setErrorHandler(eh);
				}
			}
		}
	}

	/**
	 * 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.layout == null) {
			errorHandler.error("No layout set for the appender named [" + name
					+ "].");
			return false;
		}
		return true;
	}

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

	/**
	 * 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.
	 */
	@Override
	public void append(LoggingEvent event) {

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

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

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

	@Override
	public boolean requiresLayout() {
		return false;
	}

	/**
	 * The <b>Append</b> option takes a boolean value. It is set to
	 * <code>true</code> by default. If true, then <code>File</code> will be
	 * opened in append mode by {@link #setFile setFile} (see above). Otherwise,
	 * {@link #setFile setFile} will open <code>File</code> in truncate mode.
	 * 
	 * <p>
	 * Note: Actual opening of the file is made when {@link #activateOptions} is
	 * called, not when the options are set.
	 */
	public void setAppend(boolean flag) {
		fileAppend = flag;
	}

	/**
	 * Returns the value of the <b>Append</b> option.
	 */
	public boolean getAppend() {
		return fileAppend;
	}
}

