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

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Vector;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.CyclicBuffer;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * Sends {@link LoggingEvent} objects to a set of remote log servers, usually a
 * {@link SocketNode SocketNodes}.
 * 
 * <p>
 * Acts just like {@link SocketAppender} except that instead of connecting to a
 * given remote log server, <code>SocketHubAppender</code> accepts connections
 * from the remote log servers as clients. It can accept more than one
 * connection. When a log event is received, the event is sent to the set of
 * currently connected remote log servers. Implemented this way it does not
 * require any update to the configuration file to send data to another remote
 * log server. The remote log server simply connects to the host and port the
 * <code>SocketHubAppender</code> is running on.
 * 
 * <p>
 * The <code>SocketHubAppender</code> does not store events such that the remote
 * side will events that arrived after the establishment of its connection. Once
 * connected, events arrive in order as guaranteed by the TCP protocol.
 * 
 * <p>
 * This implementation borrows heavily from the {@link SocketAppender}.
 * 
 * <p>
 * The SocketHubAppender has the following characteristics:
 * 
 * <ul>
 * 
 * <p>
 * <li>If sent to a {@link SocketNode}, logging is non-intrusive as far as the
 * log event is concerned. In other words, the event will be logged with the
 * same time stamp, {@link org.apache.log4j.NDC}, location info as if it were
 * logged locally.
 * 
 * <p>
 * <li><code>SocketHubAppender</code> does not use a layout. It ships a
 * serialized {@link LoggingEvent} object to the remote side.
 * 
 * <p>
 * <li><code>SocketHubAppender</code> relies on the TCP protocol. Consequently,
 * if the remote side is reachable, then log events will eventually arrive at
 * remote client.
 * 
 * <p>
 * <li>If no remote clients are attached, the logging requests are simply
 * dropped.
 * 
 * <p>
 * <li>Logging events are automatically <em>buffered</em> by the native TCP
 * implementation. This means that if the link to remote client is slow but
 * still faster than the rate of (log) event production, the application will
 * not be affected by the slow network connection. However, if the network
 * connection is slower then the rate of event production, then the local
 * application can only progress at the network rate. In particular, if the
 * network link to the the remote client is down, the application will be
 * blocked.
 * 
 * <p>
 * On the other hand, if the network link is up, but the remote client is down,
 * the client will not be blocked when making log requests but the log events
 * will be lost due to client unavailability.
 * 
 * <p>
 * The single remote client case extends to multiple clients connections. The
 * rate of logging will be determined by the slowest link.
 * 
 * <p>
 * <li>If the JVM hosting the <code>SocketHubAppender</code> exits before the
 * <code>SocketHubAppender</code> is closed either explicitly or subsequent to
 * garbage collection, then there might be untransmitted data in the pipe which
 * might be lost. This is a common problem on Windows based systems.
 * 
 * <p>
 * To avoid lost data, it is usually sufficient to {@link #close} the
 * <code>SocketHubAppender</code> either explicitly or by calling the
 * {@link org.apache.log4j.LogManager#shutdown} method before exiting the
 * application.
 * 
 * </ul>
 * 
 * @author Mark Womack
 */

public class SocketHubAppender extends AppenderSkeleton {

    /**
     * The default port number of the ServerSocket will be created on.
     */
    static final int DEFAULT_PORT = 4560;

    private int port = DEFAULT_PORT;
    private Vector oosList = new Vector();
    private ServerMonitor serverMonitor = null;
    private boolean locationInfo = false;
    private CyclicBuffer buffer = null;
    private String application;
    private boolean advertiseViaMulticastDNS;
    private ZeroConfSupport zeroConf;

    /**
     * The MulticastDNS zone advertised by a SocketHubAppender
     */
    public static final String ZONE = "_log4j_obj_tcpaccept_appender.local.";
    private ServerSocket serverSocket;

    public SocketHubAppender() {
    }

    /**
     * Connects to remote server at <code>address</code> and <code>port</code>.
     */
    public SocketHubAppender(int _port) {
	port = _port;
	startServer();
    }

    /**
     * Set up the socket server on the specified port.
     */
    public void activateOptions() {
	if (advertiseViaMulticastDNS) {
	    zeroConf = new ZeroConfSupport(ZONE, port, getName());
	    zeroConf.advertise();
	}
	startServer();
    }

    /**
     * Close this appender.
     * <p>
     * This will mark the appender as closed and call then {@link #cleanUp} method.
     */
    synchronized public void close() {
	if (closed)
	    return;

	LogLog.debug("closing SocketHubAppender " + getName());
	this.closed = true;
	if (advertiseViaMulticastDNS) {
	    zeroConf.unadvertise();
	}
	cleanUp();

	LogLog.debug("SocketHubAppender " + getName() + " closed");
    }

    /**
     * Release the underlying ServerMonitor thread, and drop the connections to all
     * connected remote servers.
     */
    public void cleanUp() {
	// stop the monitor thread
	LogLog.debug("stopping ServerSocket");
	serverMonitor.stopMonitor();
	serverMonitor = null;

	// close all of the connections
	LogLog.debug("closing client connections");
	while (oosList.size() != 0) {
	    ObjectOutputStream oos = (ObjectOutputStream) oosList.elementAt(0);
	    if (oos != null) {
		try {
		    oos.close();
		} catch (InterruptedIOException e) {
		    Thread.currentThread().interrupt();
		    LogLog.error("could not close oos.", e);
		} catch (IOException e) {
		    LogLog.error("could not close oos.", e);
		}

		oosList.removeElementAt(0);
	    }
	}
    }

    /**
     * Append an event to all of current connections.
     */
    public void append(LoggingEvent event) {
	if (event != null) {
	    // set up location info if requested
	    if (locationInfo) {
		event.getLocationInformation();
	    }
	    if (application != null) {
		event.setProperty("application", application);
	    }
	    event.getNDC();
	    event.getThreadName();
	    event.getMDCCopy();
	    event.getRenderedMessage();
	    event.getThrowableStrRep();

	    if (buffer != null) {
		buffer.add(event);
	    }
	}

	// if no event or no open connections, exit now
	if ((event == null) || (oosList.size() == 0)) {
	    return;
	}

	// loop through the current set of open connections, appending the event to each
	for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {

	    ObjectOutputStream oos = null;
	    try {
		oos = (ObjectOutputStream) oosList.elementAt(streamCount);
	    } catch (ArrayIndexOutOfBoundsException e) {
		// catch this, but just don't assign a value
		// this should not really occur as this method is
		// the only one that can remove oos's (besides cleanUp).
	    }

	    // list size changed unexpectedly? Just exit the append.
	    if (oos == null)
		break;

	    try {
		oos.writeObject(event);
		oos.flush();
		// Failing to reset the object output stream every now and
		// then creates a serious memory leak.
		// right now we always reset. TODO - set up frequency counter per oos?
		oos.reset();
	    } catch (IOException e) {
		if (e instanceof InterruptedIOException) {
		    Thread.currentThread().interrupt();
		}
		// there was an io exception so just drop the connection
		oosList.removeElementAt(streamCount);
		LogLog.debug("dropped connection");

		// decrement to keep the counter in place (for loop always increments)
		streamCount--;
	    }
	}
    }

    /**
     * The SocketHubAppender does not use a layout. Hence, this method returns
     * <code>false</code>.
     */
    public boolean requiresLayout() {
	return false;
    }

    /**
     * The <b>Port</b> option takes a positive integer representing the port where
     * the server is waiting for connections.
     */
    public void setPort(int _port) {
	port = _port;
    }

    /**
     * The <b>App</b> option takes a string value which should be the name of the
     * application getting logged. If property was already set (via system
     * property), don't set here.
     */
    public void setApplication(String lapp) {
	this.application = lapp;
    }

    /**
     * Returns value of the <b>Application</b> option.
     */
    public String getApplication() {
	return application;
    }

    /**
     * Returns value of the <b>Port</b> option.
     */
    public int getPort() {
	return port;
    }

    /**
     * The <b>BufferSize</b> option takes a positive integer representing the number
     * of events this appender will buffer and send to newly connected clients.
     */
    public void setBufferSize(int _bufferSize) {
	buffer = new CyclicBuffer(_bufferSize);
    }

    /**
     * Returns value of the <b>bufferSize</b> option.
     */
    public int getBufferSize() {
	if (buffer == null) {
	    return 0;
	} else {
	    return buffer.getMaxSize();
	}
    }

    /**
     * The <b>LocationInfo</b> option takes a boolean value. If true, the
     * information sent to the remote host will include location information. By
     * default no location information is sent to the server.
     */
    public void setLocationInfo(boolean _locationInfo) {
	locationInfo = _locationInfo;
    }

    /**
     * Returns value of the <b>LocationInfo</b> option.
     */
    public boolean getLocationInfo() {
	return locationInfo;
    }

    public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
	this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
    }

    public boolean isAdvertiseViaMulticastDNS() {
	return advertiseViaMulticastDNS;
    }

    /**
     * Start the ServerMonitor thread.
     */
    private void startServer() {
	serverMonitor = new ServerMonitor(port, oosList);
    }

    /**
     * Creates a server socket to accept connections.
     * 
     * @param socketPort port on which the socket should listen, may be zero.
     * @return new socket.
     * @throws IOException IO error when opening the socket.
     */
    protected ServerSocket createServerSocket(final int socketPort) throws IOException {
	return new ServerSocket(socketPort);
    }

    /**
     * This class is used internally to monitor a ServerSocket and register new
     * connections in a vector passed in the constructor.
     */
    private class ServerMonitor implements Runnable {
	private int port;
	private Vector oosList;
	private boolean keepRunning;
	private Thread monitorThread;

	/**
	 * Create a thread and start the monitor.
	 */
	public ServerMonitor(int _port, Vector _oosList) {
	    port = _port;
	    oosList = _oosList;
	    keepRunning = true;
	    monitorThread = new Thread(this);
	    monitorThread.setDaemon(true);
	    monitorThread.setName("SocketHubAppender-Monitor-" + port);
	    monitorThread.start();
	}

	/**
	 * Stops the monitor. This method will not return until the thread has finished
	 * executing.
	 */
	public synchronized void stopMonitor() {
	    if (keepRunning) {
		LogLog.debug("server monitor thread shutting down");
		keepRunning = false;
		try {
		    if (serverSocket != null) {
			serverSocket.close();
			serverSocket = null;
		    }
		} catch (IOException ioe) {
		}

		try {
		    monitorThread.join();
		} catch (InterruptedException e) {
		    Thread.currentThread().interrupt();
		    // do nothing?
		}

		// release the thread
		monitorThread = null;
		LogLog.debug("server monitor thread shut down");
	    }
	}

	private void sendCachedEvents(ObjectOutputStream stream) throws IOException {
	    if (buffer != null) {
		for (int i = 0; i < buffer.length(); i++) {
		    stream.writeObject(buffer.get(i));
		}
		stream.flush();
		stream.reset();
	    }
	}

	/**
	 * Method that runs, monitoring the ServerSocket and adding connections as they
	 * connect to the socket.
	 */
	public void run() {
	    serverSocket = null;
	    try {
		serverSocket = createServerSocket(port);
		serverSocket.setSoTimeout(1000);
	    } catch (Exception e) {
		if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
		    Thread.currentThread().interrupt();
		}
		LogLog.error("exception setting timeout, shutting down server socket.", e);
		keepRunning = false;
		return;
	    }

	    try {
		try {
		    serverSocket.setSoTimeout(1000);
		} catch (SocketException e) {
		    LogLog.error("exception setting timeout, shutting down server socket.", e);
		    return;
		}

		while (keepRunning) {
		    Socket socket = null;
		    try {
			socket = serverSocket.accept();
		    } catch (InterruptedIOException e) {
			// timeout occurred, so just loop
		    } catch (SocketException e) {
			LogLog.error("exception accepting socket, shutting down server socket.", e);
			keepRunning = false;
		    } catch (IOException e) {
			LogLog.error("exception accepting socket.", e);
		    }

		    // if there was a socket accepted
		    if (socket != null) {
			try {
			    InetAddress remoteAddress = socket.getInetAddress();
			    LogLog.debug("accepting connection from " + remoteAddress.getHostName() + " ("
				    + remoteAddress.getHostAddress() + ")");

			    // create an ObjectOutputStream
			    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
			    if (buffer != null && buffer.length() > 0) {
				sendCachedEvents(oos);
			    }

			    // add it to the oosList. OK since Vector is synchronized.
			    oosList.addElement(oos);
			} catch (IOException e) {
			    if (e instanceof InterruptedIOException) {
				Thread.currentThread().interrupt();
			    }
			    LogLog.error("exception creating output stream on socket.", e);
			}
		    }
		}
	    } finally {
		// close the socket
		try {
		    serverSocket.close();
		} catch (InterruptedIOException e) {
		    Thread.currentThread().interrupt();
		} catch (IOException e) {
		    // do nothing with it?
		}
	    }
	}
    }
}
