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


import org.apache.log4j.component.plugins.Pauseable;
import org.apache.log4j.component.plugins.Plugin;
import org.apache.log4j.component.plugins.Receiver;
import org.apache.log4j.net.ZeroConfSupport;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.LoggingEvent;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;


/**
  SocketReceiver receives a remote logging event on a configured
  socket and "posts" it to a LoggerRepository as if the event was
  generated locally. This class is designed to receive events from
  the SocketAppender class (or classes that send compatible events).

  <p>Once the event has been "posted", it will be handled by the
  appenders currently configured in the LoggerRespository.

  @author Mark Womack
  @author Scott Deboy (sdeboy@apache.org)
  @author Paul Smith (psmith@apache.org)
*/
public class SocketReceiver extends Receiver implements Runnable, PortBased,
        Pauseable {
    /**
     * socket map.
     */
  private Map socketMap = new HashMap();
    /**
     * Paused.
     */
  private boolean paused;
    /**
     * Thread.
     */
  private Thread rThread;
    /**
     * Port.
     */
  protected int port;
    /**
     * Server socket.
     */
  private ServerSocket serverSocket;
    /**
     * Socket list.
     */
  private Vector socketList = new Vector();

  /**
   * The MulticastDNS zone advertised by a SocketReceiver
   */
  public static final String ZONE = "_log4j_obj_tcpaccept_receiver.local.";

    /**
     * Listener.
     */
  private SocketNodeEventListener listener = null;
    /**
     * Listeners.
     */
  private List listenerList = Collections.synchronizedList(new ArrayList());
  private boolean advertiseViaMulticastDNS;
  private ZeroConfSupport zeroConf;

    /**
     * Create new instance.
     */
  public SocketReceiver() {
        super();
  }

    /**
     * Create new instance.
     * @param p port
     */
  public SocketReceiver(final int p) {
    super();
    port = p;
  }

    /**
     * Create new instance.
     * @param p port
     * @param repo logger repository
     */
  public SocketReceiver(final int p, final LoggerRepository repo) {
    super();
    this.port = p;
    repository = repo;
  }

  public int getPort() {
    return port;
  }

  public void setPort(final int p) {
    port = p;
  }

  /**
   * Returns true if the receiver is the same class and they are
   * configured for the same properties, and super class also considers
   * them to be equivalent. This is used by PluginRegistry when determining
   * if the a similarly configured receiver is being started.
   *
   * @param testPlugin The plugin to test equivalency against.
   * @return boolean True if the testPlugin is equivalent to this plugin.
   */
  public boolean isEquivalent(final Plugin testPlugin) {
    if ((testPlugin != null) && testPlugin instanceof SocketReceiver) {
      SocketReceiver sReceiver = (SocketReceiver) testPlugin;

      return (port == sReceiver.getPort() && super.isEquivalent(testPlugin));
    }

    return false;
  }

  /**
    Starts the SocketReceiver with the current options. */
  public void activateOptions() {
    if (!isActive()) {
      //      shutdown();
      rThread = new Thread(this);
      rThread.setDaemon(true);
      rThread.start();
      if (advertiseViaMulticastDNS) {
        zeroConf = new ZeroConfSupport(ZONE, port, getName());
        zeroConf.advertise();
      }

      active = true;
    }
  }

  /**
   * Called when the receiver should be stopped. Closes the
   * server socket and all of the open sockets.
   */
  public synchronized void shutdown() {
    getLogger().debug(getName() + " received shutdown request");

    // mark this as no longer running
    active = false;

    if (rThread != null) {
      rThread.interrupt();
      rThread = null;
    }
    if (advertiseViaMulticastDNS) {
        zeroConf.unadvertise();
    }

    doShutdown();
  }

  /**
   * Does the actual shutting down by closing the server socket
   * and any connected sockets that have been created.
   */
  private synchronized void doShutdown() {
    active = false;

    getLogger().debug(getName() + " doShutdown called");

    // close the server socket
    closeServerSocket();

    // close all of the accepted sockets
    closeAllAcceptedSockets();
  }

  /**
   * Closes the server socket, if created.
   */
  private void closeServerSocket() {
    getLogger().debug("{} closing server socket", getName());

    try {
      if (serverSocket != null) {
        serverSocket.close();
      }
    } catch (Exception e) {
      // ignore for now
    }

    serverSocket = null;
  }

  /**
   * Closes all the connected sockets in the List.
   */
  private synchronized void closeAllAcceptedSockets() {
    for (int x = 0; x < socketList.size(); x++) {
      try {
        ((Socket) socketList.get(x)).close();
      } catch (Exception e) {
        // ignore for now
      }
    }

    // clear member variables
    socketMap.clear();
    socketList.clear();
  }

  /**
    Sets the flag to indicate if receiver is active or not.
   @param b new value
   */
  protected synchronized void setActive(final boolean b) {
    active = b;
  }

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

  public boolean isAdvertiseViaMulticastDNS() {
      return advertiseViaMulticastDNS;
  }

  /**
    Loop, accepting new socket connections. */
  public void run() {
    /**
     * Ensure we start fresh.
     */
    closeServerSocket();
    closeAllAcceptedSockets();

    // start the server socket
    try {
      serverSocket = new ServerSocket(port);
    } catch (Exception e) {
      getLogger().error(
        "error starting SocketReceiver (" + this.getName()
        + "), receiver did not start", e);
      active = false;

      return;
    }

    Socket socket = null;

    try {
      getLogger().debug("in run-about to enter while not interrupted loop");

      active = true;

      while (!rThread.isInterrupted()) {
        // if we have a socket, start watching it
        if (socket != null) {
          getLogger().debug(
                  "socket not null - creating and starting socketnode");
          socketList.add(socket);

          SocketNode13 node = new SocketNode13(socket, this);
          synchronized (listenerList) {
            for (Iterator iter = listenerList.iterator();
                 iter.hasNext();) {
                SocketNodeEventListener l =
                        (SocketNodeEventListener) iter.next();
                node.addSocketNodeEventListener(l);
            }
          }
          socketMap.put(socket, node);
          new Thread(node).start();
          socket = null;
        }

        getLogger().debug("waiting to accept socket");

        // wait for a socket to open, then loop to start it
        socket = serverSocket.accept();
        getLogger().debug("accepted socket");
      }
    } catch (Exception e) {
      getLogger().warn(
        "exception while watching socket server in SocketReceiver ("
        + this.getName() + "), stopping");
    }

    getLogger().debug("{} has exited the not interrupted loop", getName());

    // socket not watched because we a no longer running
    // so close it now.
    if (socket != null) {
      try {
        socket.close();
      } catch (IOException e1) {
        getLogger().warn("socket exception caught - socket closed");
      }
    }

    getLogger().debug("{} is exiting main run loop", getName());
  }

  /**
   * Returns a Vector of SocketDetail representing the IP/Domain name
   * of the currently connected sockets that this receiver has
   * been responsible for creating.
   * @return Vector of SocketDetails
   */
  public Vector getConnectedSocketDetails() {
    Vector details = new Vector(socketList.size());

    for (Enumeration enumeration = socketList.elements();
         enumeration.hasMoreElements();
            ) {
      Socket socket = (Socket) enumeration.nextElement();
      details.add(
        new SocketDetail(socket, (SocketNode13) socketMap.get(socket)));
    }

    return details;
  }

  /**
   * Returns the currently configured SocketNodeEventListener that
   * will be automatically set for each SocketNode created.
   * @return SocketNodeEventListener currently configured
   *
   * @deprecated This receiver now supports multiple listeners
   */
  public SocketNodeEventListener getListener() {
    return listener;
  }

  /**
   * Adds the listener to the list of listeners to be notified of the
   * respective event.
   * @param l the listener to add to the list
   */
  public void addSocketNodeEventListener(
          final SocketNodeEventListener l) {
    listenerList.add(l);
  }

  /**
   * Removes the registered Listener from this instances list of
   * listeners.  If the listener has not been registered, then invoking
   * this method has no effect.
   *
   * @param l the SocketNodeEventListener to remove
   */
  public void removeSocketNodeEventListener(
          final SocketNodeEventListener l) {
    listenerList.remove(l);
  }

  /**
   * Sets the SocketNodeEventListener that will be used for each
   * created SocketNode.
   * @param l the listener to set on each creation of a SocketNode
   * @deprecated This receiver now supports multiple listeners and
   * so this method simply removes the listener (if there already)
   * and readds it to the list.
   *
   * The passed listener will also be returned via the getListener()
   * method still, but this is also deprecated
   */
  public void setListener(final SocketNodeEventListener l) {
    removeSocketNodeEventListener(l);
    addSocketNodeEventListener(l);
    this.listener = l;
  }

    /** {@inheritDoc} */
  public boolean isPaused() {
    return paused;
  }

    /** {@inheritDoc} */
  public void setPaused(final boolean b) {
    paused = b;
  }

    /**
     * Socket detail.
     */
  private static final class SocketDetail implements AddressBased, PortBased,
    Pauseable {
      /**
       * Address.
       */
    private String address;
      /**
       * Port.
       */
    private int port;
      /**
       * Socket node.
       */
    private SocketNode13 socketNode;

      /**
       * Create new instance.
       * @param socket socket
       * @param node socket node
       */
    private SocketDetail(final Socket socket,
                         final SocketNode13 node) {
      super();
      this.address = socket.getInetAddress().getHostName();
      this.port = socket.getPort();
      this.socketNode = node;
    }

      /** {@inheritDoc} */
    public String getAddress() {
      return address;
    }

      /** {@inheritDoc} */
    public int getPort() {
      return port;
    }

      /** {@inheritDoc} */
    public String getName() {
      return "Socket";
    }

      /** {@inheritDoc} */
    public boolean isActive() {
      return true;
    }

      /** {@inheritDoc} */
    public boolean isPaused() {
      return socketNode.isPaused();
    }

      /** {@inheritDoc} */
    public void setPaused(final boolean b) {
      socketNode.setPaused(b);
    }
  }
    /** {@inheritDoc} */
  public void doPost(final LoggingEvent event) {
    if (!isPaused()) {
      super.doPost(event);
    }
  }

}
