/*
 * Copyright (C) Telemics Inc. All rights reserved.
 *
 * This software is published under the terms of the Apache Software
 * License version 1.1, a copy of which has been included with this
 * distribution in the LICENSE.APL file.   */

package org.apache.log4j.net;

import java.net.InetAddress;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.io.IOException;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.Category;
import org.apache.log4j.Priority;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Layout;

/**
    Sends log information as a UPD datagrams.

    <p>The UDPAppender is meant to be used as a diagnostic logging tool
    so that logging can be monitored by a simple UDP client.
    
    <p>Messages are not sent as LoggingEvent objects but as text after
    applying the designated Layout.
    
    <p>The port and remoteHost properties can be set in configuration properties.
    By setting the remoteHost to a broadcast address any number of clients can
    listen for log messages.
    
    <p>This was inspired and really extended/copied from {@link SocketAppender}.  Please
    see the docs for the proper credit to the authors of that class.
    
    @author  <a href="mailto:kbrown@versatilesolutions.com">Kevin Brown</a>
    
    */

public class UDPAppender extends AppenderSkeleton {
  /**
     The default port number for the UDP packets. (9991).
  */
  static final int DEFAULT_PORT                 = 9991;
  
  /**
     The default reconnection delay (30000 milliseconds or 30 seconds).
  */
  static final int DEFAULT_RECONNECTION_DELAY   = 30000;

  /**
     We remember host name as String in addition to the resolved
     InetAddress so that it can be returned via getOption().
  */
  String remoteHost;
  
  InetAddress address;
  int port = DEFAULT_PORT;
  DatagramSocket outSocket;
  int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
  boolean locationInfo = false;

  private Connector connector;

  public UDPAppender() {
  }

  /**
     Sends UDP packets to the <code>address</code> and <code>port</code>.
  */
  public
  UDPAppender(InetAddress address, int port) {
    this.address = address;
    this.remoteHost = address.getHostName();
    this.port = port;
    connect(address, port);
  }

  /**
     Sends UDP packets to the <code>address</code> and <code>port</code>.
  */
  public
  UDPAppender(String host, int port) { 
    this.port = port;
    this.address = getAddressByName(host);
    this.remoteHost = host;
    connect(address, port);
  }
  
  /**
     Open the UDP sender for the <b>RemoteHost</b> and <b>Port</b>. 
  */
  public
  void activateOptions() {
    connect(address, port);
  }

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

    this.closed = true;
    cleanUp();
  }

  /**
     Close the UDP Socket and release the underlying
     connector thread if it has been created
   */
  public 
  void cleanUp() {
    if(outSocket != null) {
      try {
	      outSocket.close();
      }
      catch(Exception e) {
	      LogLog.error("Could not close outSocket.", e);
      }
      outSocket = null;      
    }
    if(connector != null) {
      //LogLog.debug("Interrupting the connector.");      
      connector.interrupted = true;
      connector = null;  // allow gc
    }
  }

  void connect(InetAddress address, int port) {
    if(this.address == null)
      return;
    try {
      // First, close the previous connection if any.
      cleanUp();          
      outSocket = new DatagramSocket();
    }
    catch(IOException e) {
      LogLog.error("Could not open UDP Socket for sending. We will try again later.", e);
      fireConnector();
    }
  }


  public
  void append(LoggingEvent event) {
    if(event == null)
      return;

    if(address==null) {
      errorHandler.error("No remote host is set for UDPAppender named \""+
			this.name+"\".");
      return;
    }

    if(outSocket != null) {
      try {
          byte[] logData = this.layout.format(event).getBytes("ASCII");
          DatagramPacket dp = new DatagramPacket(logData, logData.length, address, port);
          
          outSocket.send(dp);
          
          if(layout.ignoresThrowable()) {
            String[] s = event.getThrowableStrRep();
            if (s != null) {
               byte[] ls = Layout.LINE_SEP.getBytes("ASCII");
               DatagramPacket lineSepDP = new DatagramPacket(ls, ls.length, address, port);
               int len = s.length;
               for(int i = 0; i < len; i++) {
                  logData = s[i].getBytes("ASCII");
                  dp = new DatagramPacket(logData, logData.length, address, port);
                  outSocket.send(dp);
                  outSocket.send(lineSepDP);
               }
            }
          }
      }
      catch(IOException e) {
         outSocket = null;
         LogLog.warn("Detected problem with UDP connection: "+e);
         if(reconnectionDelay > 0) {
           fireConnector();
         }
      }
    }
  }

  void fireConnector() {
    if(connector == null) {
      LogLog.debug("Starting a new connector thread.");
      connector = new Connector();
      connector.setDaemon(true);
      connector.setPriority(Thread.MIN_PRIORITY);
      connector.start();      
    }
  }
  
  static
  InetAddress getAddressByName(String host) {
    try {
      return InetAddress.getByName(host);
    }	
    catch(Exception e) {
      LogLog.error("Could not find address of ["+host+"].", e);
      return null;
    }
  }

  /**
     The UDPAppender uses layouts. Hence, this method returns
     <code>true</code>.
  */
  public
  boolean requiresLayout() {
    return true;
  }

  /**
     The <b>RemoteHost</b> option takes a string value which should be
     the host name or ipaddress to send the UDP packets.
   */
  public
  void setRemoteHost(String host) {
    address = getAddressByName(host);
    remoteHost = host;
  }
  
  /**
     Returns value of the <b>RemoteHost</b> option.
   */
  public
  String getRemoteHost() {
    return remoteHost;
  }
  
  /**
     The <b>Port</b> option takes a positive integer representing
     the port where UDP packets will be sent.
   */
  public
  void setPort(int port) {
    this.port = port;
  }
  
  /**
     Returns value of the <b>Port</b> option.
   */
  public
  int getPort() {
    return port;
  }
  
  /**
     The <b>ReconnectionDelay</b> option takes a positive integer
     representing the number of milliseconds to wait between each
     failed attempt to establish an outgoing socket. The default value of
     this option is 30000 which corresponds to 30 seconds.
     
     <p>Setting this option to zero turns off reconnection
     capability.
   */
  public
  void setReconnectionDelay(int delay) {
    this.reconnectionDelay = delay;
  }
  
  /**
     Returns value of the <b>ReconnectionDelay</b> option.
   */
  public
  int getReconnectionDelay() {
    return reconnectionDelay;
  }
  
  /**
     The Connector will retry the UDP socket.
     It does this by attempting to open a new UDP socket every
     <code>reconnectionDelay</code> milliseconds.

     <p>It stops trying whenever a connection is established. It will
     restart to try reconnect to the server when previpously open
     connection is droppped.

     @author  Ceki G&uuml;lc&uuml; 
     @since 0.8.4
  */
  class Connector extends Thread {

    boolean interrupted = false;

    public
    void run() {
      DatagramSocket socket;      
      while(!interrupted) {
        try {
           sleep(reconnectionDelay);
           LogLog.debug("Attempting to establish UDP Datagram Socket");
           socket = new DatagramSocket();
           synchronized(this) {
             outSocket = socket; 
             connector = null;
             break;
           }
        }
        catch(InterruptedException e) {
           LogLog.debug("Connector interrupted. Leaving loop.");
           return;
        }
        catch(IOException e) {	  
         LogLog.debug("Could not establish an outgoing DatagramSocket." + e);
        }
       }
         //LogLog.debug("Exiting Connector.run() method.");
    }
  }
}

