/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2016, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

package org.mobicents.ha.javax.sip;

import gov.nist.core.CommonLogger;
import gov.nist.core.StackLogger;
import gov.nist.javax.sip.ListeningPointExt;

import java.io.IOException;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.sip.ListeningPoint;

import org.mobicents.ha.javax.sip.util.Inet6Util;
import org.mobicents.tools.sip.balancer.NodeRegisterRMIStub;
import org.mobicents.tools.sip.balancer.SIPNode;

/**
 *
implementation of the LoadBalancerHeartBeatingService interface.

 *
 *

 *  It sends heartbeats and health information to the sip balancers configured through the stack property org.mobicents.ha.javax.sip.BALANCERS
 *

 *
 * @author Jean Deruelle
 *
 */
public class LoadBalancerHeartBeatingServiceImpl implements LoadBalancerHeartBeatingService, LoadBalancerHeartBeatingServiceImplMBean {

	private static StackLogger logger = CommonLogger.getLogger(LoadBalancerHeartBeatingServiceImpl.class);
	public static String LB_HB_SERVICE_MBEAN_NAME = "org.mobicents.jain.sip:type=load-balancer-heartbeat-service,name=";
    public final static String REACHABLE_CHECK = "org.mobicents.ha.javax.sip.REACHABLE_CHECK";
    public final static String LOCAL_HTTP_PORT = "org.mobicents.ha.javax.sip.LOCAL_HTTP_PORT";
    public final static String LOCAL_SSL_PORT = "org.mobicents.ha.javax.sip.LOCAL_SSL_PORT";
    public final static String LOCAL_SMPP_PORT = "org.mobicents.ha.javax.sip.LOCAL_SMPP_PORT";
    public final static String LOCAL_SMPP_SSL_PORT = "org.mobicents.ha.javax.sip.LOCAL_SMPP_SSL_PORT";
    public final static String SOCKET_BINDING_GROUP = "org.mobicents.ha.javax.sip.SOCKET_BINDING_GROUP";

	public static final int DEFAULT_RMI_PORT = 2000;
	public static final String BALANCER_SIP_PORT_CHAR_SEPARATOR = ":";
	public static final String BALANCERS_CHAR_SEPARATOR = ";";
	public static final int DEFAULT_LB_SIP_PORT = 5065;
	public static final int DEFAULT_LB_HTTP_PORT = 2080;

	ClusteredSipStack sipStack = null;
    Properties sipStackProperties = null;
	//the logger
    //the balancers to send heartbeat to and our health info
	protected String balancers;
	//the jvmRoute for this node
	protected String jvmRoute;
    //the balancers names to send heartbeat to and our health info
	protected Map<String, SipLoadBalancer> register = new ConcurrentHashMap<String, SipLoadBalancer>();
	//heartbeat interval, can be modified through JMX
	protected long heartBeatInterval = 5000;
	protected Timer heartBeatTimer = new Timer();
	protected TimerTask hearBeatTaskToRun = null;
	protected List<String> cachedAnyLocalAddresses = new ArrayList<String>();
	protected boolean started = false;
    protected boolean gracefullyShuttingDown = false;
    protected boolean reachableCheck = true;
    protected String sessionId;

	protected Set<LoadBalancerHeartBeatingListener> loadBalancerHeartBeatingListeners;
	// https://github.com/RestComm/jain-sip.ha/issues/3
	protected boolean useLoadBalancerForAllConnectors;
	protected Set<ListeningPoint> sipConnectors;
	// https://github.com/RestComm/jain-sip.ha/issues/4 :
	// Caching the sipNodes to send to the LB as there is no reason for them to change often or at all after startup
	ConcurrentHashMap<String, SIPNode> sipNodes = new ConcurrentHashMap<String, SIPNode>();

	ObjectName oname = null;

    public LoadBalancerHeartBeatingServiceImpl() {
		loadBalancerHeartBeatingListeners = new CopyOnWriteArraySet<LoadBalancerHeartBeatingListener>();
		sipConnectors = new CopyOnWriteArraySet<ListeningPoint>();
		useLoadBalancerForAllConnectors = true;
	}

	public void init(ClusteredSipStack clusteredSipStack,
			Properties stackProperties) {
		sipStack = clusteredSipStack;
        sipStackProperties = stackProperties;
		balancers = stackProperties.getProperty(BALANCERS);
		heartBeatInterval = Integer.parseInt(stackProperties.getProperty(HEARTBEAT_INTERVAL, "5000"));
        String reachableCheckString = sipStackProperties.getProperty(REACHABLE_CHECK);
        if(reachableCheckString != null) {
            reachableCheck = Boolean.parseBoolean(reachableCheckString);
        }
        logger.logInfo("Reachable checks " + reachableCheck);
	}

	public void stopBalancer() {
		stop();
	}

    public void start() {
      	Runtime.getRuntime().addShutdownHook(new Thread() {
    	    public void run() {
    	    	stopBalancer();
    	    	logger.logInfo("Shutting down the Load Balancer Link");
            }
    	});

    	if (!started) {
			if (balancers != null && balancers.length() > 0) {
				String[] balancerDescriptions = balancers.split(BALANCERS_CHAR_SEPARATOR);
				for (String balancerDescription : balancerDescriptions) {
					String balancerAddress = balancerDescription;
					int sipPort = DEFAULT_LB_SIP_PORT;
					int httpPort = DEFAULT_LB_HTTP_PORT;
					int rmiPort = DEFAULT_RMI_PORT;
					if(balancerDescription.indexOf(BALANCER_SIP_PORT_CHAR_SEPARATOR) != -1) {
						String[] balancerDescriptionSplitted = balancerDescription.split(BALANCER_SIP_PORT_CHAR_SEPARATOR);
						balancerAddress = balancerDescriptionSplitted[0];
						try {
							//sipPort:httpPort:rmiPort
							sipPort = Integer.parseInt(balancerDescriptionSplitted[1]);
							if(balancerDescriptionSplitted.length>2) {
								httpPort = Integer.parseInt(balancerDescriptionSplitted[2]);
								if(balancerDescriptionSplitted.length>3){
									rmiPort = Integer.parseInt(balancerDescriptionSplitted[3]);
								}
							}
						} catch (NumberFormatException e) {
							logger.logError("Impossible to parse the following sip balancer port " + balancerDescriptionSplitted[1], e);
						}
					}
					if(Inet6Util.isValidIP6Address(balancerAddress) || Inet6Util.isValidIPV4Address(balancerAddress)) {
						try {
							this.addBalancer(InetAddress.getByName(balancerAddress).getHostAddress(), sipPort, httpPort, rmiPort);
						} catch (UnknownHostException e) {
							logger.logError("Impossible to parse the following sip balancer address " + balancerAddress, e);
						}
					} else {
						this.addBalancer(balancerAddress, sipPort, httpPort, 0, rmiPort);
					}
				}
			}
			started = true;
			sessionId = ""+System.currentTimeMillis();
		}
    	if(sipNodes.isEmpty()) {
    		logger.logInfo("Computing SIP Nodes to be sent to the LB");
    		updateConnectorsAsSIPNode();
		}
		this.hearBeatTaskToRun = new BalancerPingTimerTask();

		// Delay the start with 2 seconds so nodes joining under load are really ready to serve requests
		// Otherwise one of the listeneing points comes a bit later and results in errors.
		this.heartBeatTimer.scheduleAtFixedRate(this.hearBeatTaskToRun, this.heartBeatInterval,
				this.heartBeatInterval);
		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("Created and scheduled tasks for sending heartbeats to the sip balancer every " + heartBeatInterval + "ms.");
		}

		registerMBean();

		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("Load Balancer Heart Beating Service has been started");
		}
    }

    public void stop() {
    	// Force removal from load balancer upon shutdown
    	// added for Issue 308 (http://code.google.com/p/mobicents/issues/detail?id=308)
    	removeNodesFromBalancers();
    	//cleaning
//    	balancerNames.clear();
    	register.clear();
    	if(hearBeatTaskToRun != null) {
    		this.hearBeatTaskToRun.cancel();
    	}
		this.hearBeatTaskToRun = null;
		loadBalancerHeartBeatingListeners.clear();
		started = false;

		heartBeatTimer.cancel();

		unRegisterMBean();

		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("Load Balancer Heart Beating Service has been stopped");
		}
    }

    protected void registerMBean() {
    	String mBeanName = LB_HB_SERVICE_MBEAN_NAME + sipStack.getStackName();
		try {
			oname = new ObjectName(mBeanName);
			if (sipStack.getMBeanServer() != null && !sipStack.getMBeanServer().isRegistered(oname)) {
				sipStack.getMBeanServer().registerMBean(this, oname);
			}
		} catch (Exception e) {
			logger.logError("Could not register the Load Balancer Service as an MBean under the following name " + mBeanName, e);
		}
	}

	protected void unRegisterMBean() {
		String mBeanName = LB_HB_SERVICE_MBEAN_NAME + sipStack.getStackName();
		try {
			if (oname != null && sipStack.getMBeanServer() != null && sipStack.getMBeanServer().isRegistered(oname)) {
				sipStack.getMBeanServer().unregisterMBean(oname);
			}
		} catch (Exception e) {
			logger.logError("Could not unregister the stack as an MBean under the following name" + mBeanName);
		}
	}

    /**
     * {@inheritDoc}
     */
	public long getHeartBeatInterval() {
		return heartBeatInterval;
	}
	/**
     * {@inheritDoc}
     */
	public void setHeartBeatInterval(long heartBeatInterval) {
		if (heartBeatInterval < 100)
			return;

		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("Setting HeartBeatInterval from " + this.heartBeatInterval + " to " + heartBeatInterval);
		}

		this.heartBeatInterval = heartBeatInterval;
		if(sipNodes.isEmpty()) {
			logger.logInfo("Computing SIP Nodes to be sent to the LB");
			updateConnectorsAsSIPNode();
		}
		this.hearBeatTaskToRun.cancel();
		this.hearBeatTaskToRun = new BalancerPingTimerTask();
		this.heartBeatTimer.scheduleAtFixedRate(this.hearBeatTaskToRun, 0,
				this.heartBeatInterval);

	}

	/**
	 *
	 * @param hostName
	 * @param index
	 * @return
	 */
	private InetAddress fetchHostAddress(String hostName, int index) {
		if (hostName == null)
			throw new NullPointerException("Host name cant be null!!!");

		InetAddress[] hostAddr = null;
		try {
			hostAddr = InetAddress.getAllByName(hostName);
		} catch (UnknownHostException uhe) {
			throw new IllegalArgumentException(
					"HostName is not a valid host name or it doesnt exists in DNS",
					uhe);
		}

		if (index < 0 || index >= hostAddr.length) {
			throw new IllegalArgumentException(
					"Index in host address array is wrong, it should be [0]<x<["
							+ hostAddr.length + "] and it is [" + index + "]");
		}

		InetAddress address = hostAddr[index];
		return address;
	}

	/**
     * {@inheritDoc}
     */
	public String[] getBalancers() {
		return this.register.keySet().toArray(new String[register.keySet().size()]);
	}

	/**
     * {@inheritDoc}
     */
	public boolean addBalancer(String addr, int sipPort, int httpPort, int rmiPort) {
		if (addr == null)
			throw new NullPointerException("addr cant be null!!!");

		InetAddress address = null;
		try {
			address = InetAddress.getByName(addr);
		} catch (UnknownHostException e) {
			throw new IllegalArgumentException(
					"Something wrong with host creation.", e);
		}
		String balancerName = address.getCanonicalHostName() + ":" + rmiPort;

		if (register.get(balancerName) != null) {
			logger.logInfo("Sip balancer " + balancerName + " already present, not added");
			return false;
		}

		SipLoadBalancer sipLoadBalancer = new SipLoadBalancer(this, address, sipPort, httpPort, rmiPort);
		register.put(balancerName, sipLoadBalancer);

		// notify the listeners
		for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
			loadBalancerHeartBeatingListener.loadBalancerAdded(sipLoadBalancer);
		}

		if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("following balancer name : " + balancerName +"/address:"+ addr + " added");
		}

		return true;
	}

	/**
     * {@inheritDoc}
     */
	public boolean addBalancer(String hostName, int sipPort, int httpPort, int index, int rmiPort) {
		return this.addBalancer(fetchHostAddress(hostName, index)
				.getHostAddress(), sipPort, httpPort, rmiPort);
	}

	/**
     * {@inheritDoc}
     */
	public boolean removeBalancer(String addr, int sipPort, int httpPort, int rmiPort) {
		if (addr == null)
			throw new NullPointerException("addr cant be null!!!");

		InetAddress address = null;
		try {
			address = InetAddress.getByName(addr);
		} catch (UnknownHostException e) {
			throw new IllegalArgumentException(
					"Something wrong with host creation.", e);
		}

		SipLoadBalancer sipLoadBalancer = new SipLoadBalancer(this, address, sipPort, httpPort, rmiPort);

		String keyToRemove = null;
		Iterator<String> keyIterator = register.keySet().iterator();
		while (keyIterator.hasNext() && keyToRemove ==null) {
			String key = keyIterator.next();
			if(register.get(key).equals(sipLoadBalancer)) {
				keyToRemove = key;
			}
		}

		if(keyToRemove !=null ) {
			register.remove(keyToRemove);

			// notify the listeners
			for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
				loadBalancerHeartBeatingListener.loadBalancerRemoved(sipLoadBalancer);
			}

			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("following balancer name : " + keyToRemove +"/address:"+ addr + " removed");
			}

			return true;
		}

		return false;
	}

	/**
     * {@inheritDoc}
     */
	public boolean removeBalancer(String hostName, int sipPort, int httpPort, int index, int rmiPort) {
		InetAddress[] hostAddr = null;
		try {
			hostAddr = InetAddress.getAllByName(hostName);
		} catch (UnknownHostException uhe) {
			throw new IllegalArgumentException(
					"HostName is not a valid host name or it doesnt exists in DNS",
					uhe);
		}

		if (index < 0 || index >= hostAddr.length) {
			throw new IllegalArgumentException(
					"Index in host address array is wrong, it should be [0]<x<["
							+ hostAddr.length + "] and it is [" + index + "]");
		}

		InetAddress address = hostAddr[index];

		return this.removeBalancer(address.getHostAddress(), sipPort, httpPort, rmiPort);
	}

	protected void updateConnectorsAsSIPNode() {
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
			logger.logTrace("Gathering all SIP Connectors information. UseLoadBalancer for all connectors: " + useLoadBalancerForAllConnectors);
		}
		// Gathering info about server' sip listening points
		Iterator<ListeningPoint> listeningPointIterator = null;
		if(useLoadBalancerForAllConnectors) {
			listeningPointIterator = sipStack.getListeningPoints();
		} else {
			listeningPointIterator = sipConnectors.iterator();
		}
		while (listeningPointIterator.hasNext()) {

			Integer sipTcpPort = null;
			Integer sipTlsPort = null;
			Integer sipSctpPort = null;
			Integer sipUdpPort = null;
			Integer sipWsPort = null;
			Integer sipWssPort = null;
			String address = null;
			String hostName = null;

			ListeningPoint listeningPoint = listeningPointIterator.next();
			address = listeningPoint.getIPAddress();

			if(address == null) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
					logger.logTrace("Address is null");
				}
			} else {
				// From Vladimir: for some reason I get "localhost" here instead of IP and this confuses the LB
				if(address.equals("localhost")) address = "127.0.0.1";

				int port = listeningPoint.getPort();
				String transport = listeningPoint.getTransport();
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
					logger.logTrace("connector: " + address + ", port: " + port + ", transport: " + transport);
				}
				if(transport.equalsIgnoreCase(ListeningPoint.TCP)) {
					sipTcpPort = port;
				} else if(transport.equalsIgnoreCase(ListeningPoint.UDP)) {
					sipUdpPort = port;
				} else if(transport.equalsIgnoreCase(ListeningPointExt.WS)) {
					sipWsPort = port;
				} else if(transport.equalsIgnoreCase(ListeningPointExt.WSS)) {
					// https://github.com/RestComm/jain-sip.ha/issues/5
					sipWssPort = port;
				} else if(transport.equalsIgnoreCase(ListeningPoint.TLS)) {
					// https://github.com/RestComm/jain-sip.ha/issues/5
					sipTlsPort = port;
				} else if(transport.equalsIgnoreCase(ListeningPoint.SCTP)) {
					// https://github.com/RestComm/jain-sip.ha/issues/5
					sipSctpPort = port;
				}

				try {
					if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
						logger.logTrace("Trying to find address array for address " + address);
					}
					InetAddress[] aArray = InetAddress
							.getAllByName(address);
					if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
						for (int i = 0; i < aArray.length; i++) {
							logger.logTrace("Found " + aArray[i].getHostAddress());
						}
					}
					if (aArray != null && aArray.length > 0) {
						// Damn it, which one we should pick?
						hostName = aArray[0].getCanonicalHostName();
					}
				} catch (UnknownHostException e) {
					logger.logError("An exception occurred while trying to retrieve the hostname of a sip connector", e);
				}

				List<String> ipAddresses = new ArrayList<String>();
				boolean isAnyLocalAddress = false;
				try {
					if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
						logger.logTrace("Trying to find if address " + address + " is an AnyLocalAddress");
					}
					isAnyLocalAddress = InetAddress.getByName(address).isAnyLocalAddress();
					if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
						logger.logTrace(address + " is an AnyLocalAddress ? " + isAnyLocalAddress);
					}
				} catch (UnknownHostException e) {
					logger.logWarning("Unable to enumerate mapped interfaces. Binding to 0.0.0.0 may not work.");
					isAnyLocalAddress = false;
				}
				if(isAnyLocalAddress) {
					if(cachedAnyLocalAddresses.isEmpty()) {
						try{
							if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
								logger.logTrace("Gathering all network interfaces");
							}
							Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
							if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
								logger.logTrace("network interfaces gathered " + networkInterfaces);
							}
							while(networkInterfaces.hasMoreElements()) {
								NetworkInterface networkInterface = networkInterfaces.nextElement();
								Enumeration<InetAddress> bindings = networkInterface.getInetAddresses();
								while(bindings.hasMoreElements()) {
									InetAddress addr = bindings.nextElement();
									String networkInterfaceIpAddress = addr.getHostAddress();
									// we cache the look up to speed up the next time
									cachedAnyLocalAddresses.add(networkInterfaceIpAddress);
								}
							}
						} catch (SocketException e) {
							logger.logWarning("Unable to enumerate network interfaces. Binding to 0.0.0.0 may not work.");
						}
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
							logger.logTrace("Adding " + cachedAnyLocalAddresses + " to the list of IPs to send");
						}
						ipAddresses.addAll(cachedAnyLocalAddresses);
					}
				} else {
					if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
						logger.logTrace("Adding " + address + " to the list of IPs to send");
					}
					ipAddresses.add(address);
				}

				String httpPortString = sipStackProperties.getProperty(LOCAL_HTTP_PORT);
				String sslPortString = sipStackProperties.getProperty(LOCAL_SSL_PORT);
				String smppPortString = sipStackProperties.getProperty(LOCAL_SMPP_PORT);
				String smppSslPortString = sipStackProperties.getProperty(LOCAL_SMPP_SSL_PORT);

				if(httpPortString == null && sslPortString == null) {
					logger.logWarning("HTTP or HTTPS port couldn't be retrieved from System properties, trying with JMX");
					Integer httpPort = null;
					Boolean httpBound = false;
					Integer sslPort = null;
					Boolean sslBound = false;

					MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
					try {
		                // https://telestax.atlassian.net/browse/JSIPHA-4 defaulting to standard-sockets
		                String socketBindingGroup = sipStackProperties.getProperty(SOCKET_BINDING_GROUP, "standard-sockets");
						ObjectName http = new ObjectName("jboss.as:socket-binding-group=" + socketBindingGroup + ",socket-binding=http");
						httpPort = (Integer) mBeanServer.getAttribute(http, "boundPort");
						httpBound = (Boolean) mBeanServer.getAttribute(http, "bound");

						ObjectName https = new ObjectName("jboss.as:socket-binding-group=" + socketBindingGroup + ",socket-binding=https");
						sslPort = (Integer) mBeanServer.getAttribute(https, "boundPort");
						sslBound = (Boolean) mBeanServer.getAttribute(https, "bound");
						if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
							logger.logTrace("Dound httpPort " + httpPort + " and sslPort " + sslPort);
						}
					} catch (Exception e) {} //Ignore any exceptions

					if(httpBound && httpPort!=null){
						sipStackProperties.setProperty(LOCAL_HTTP_PORT, String.valueOf(httpPort));
					} else if (sslBound && sslPort!=null){
		                sipStackProperties.setProperty(LOCAL_SSL_PORT, String.valueOf(sslPort));
					}
				}

				for (String ipAddress : ipAddresses) {
					if(ipAddress == null) {
						if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
							logger.logTrace("Following IpAddress [" + ipAddress + "] is null not pinging the LB for that null IP or it will cause routing issues");
						}
					} else {
						if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
							logger.logTrace("Creating new SIP Node for [" + ipAddress + "] to be added to the list for pinging the LB");
						}
						SIPNode node = new SIPNode(hostName, ipAddress);
						SIPNode previousValue = sipNodes.putIfAbsent(ipAddress, node);
						if(previousValue == null) {
							if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
								logger.logTrace("Added a sip Node with the key [" + ipAddress + "]");
							}
						} else {
							node = previousValue;
							if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
								logger.logTrace("SIPNode " + node +  " was already present");
							}
						}

						int httpPort = 0;
						int sslPort = 0;
						int smppPort = 0;
						int smppSslPort = 0;

						if(httpPortString != null) {
							httpPort = Integer.parseInt(httpPortString);
							node.getProperties().put("httpPort", httpPort);
						}
						if(sslPortString != null) {
							sslPort = Integer.parseInt(sslPortString);
							node.getProperties().put("sslPort", sslPort);
						}
						if(smppPortString != null) {
							smppPort = Integer.parseInt(smppPortString);
							node.getProperties().put("smppPort", smppPort);
						}
						if(smppSslPortString != null) {
							smppSslPort = Integer.parseInt(smppSslPortString);
							node.getProperties().put("smppSslPort", smppSslPort);
						}

						if(sipTcpPort != null) node.getProperties().put("tcpPort", sipTcpPort);
						if(sipUdpPort != null) node.getProperties().put("udpPort", sipUdpPort);
						if(sipWsPort != null) node.getProperties().put("wsPort", sipWsPort);
						// https://github.com/RestComm/jain-sip.ha/issues/5
						if(sipWssPort != null) node.getProperties().put("wssPort", sipWssPort);
						if(sipTlsPort != null) node.getProperties().put("tlsPort", sipTlsPort);
						if(sipSctpPort != null) node.getProperties().put("sctpPort", sipTlsPort);

						if(jvmRoute != null) node.getProperties().put("jvmRoute", jvmRoute);

						node.getProperties().put("version", System.getProperty("org.mobicents.server.version", "0"));
						node.getProperties().put("sessionId", sessionId);

						if(gracefullyShuttingDown) {
							if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
								logger.logDebug("Adding GRACEFUL_SHUTDOWN prop to following SIP Node " + node);
							}
							Map<String, Serializable> properties = node.getProperties();
							properties.put("GRACEFUL_SHUTDOWN", "true");
						}

						if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
							logger.logTrace("Added node [" + node + "] to the list for pinging the LB");
						}
					}
				}
			}
		}
	}

	/**
	 * @param info
	 */
	protected void sendKeepAliveToBalancers() {
		if(sipNodes.isEmpty()) {
    		logger.logInfo("Computing SIP Nodes to be sent to the LB as the list is currently empty");
    		updateConnectorsAsSIPNode();
		}
		ArrayList<SIPNode> info = new ArrayList<SIPNode>(sipNodes.values());
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
            logger.logTrace("Pinging balancers with info[" + info + "]");
        }
		Thread.currentThread().setContextClassLoader(NodeRegisterRMIStub.class.getClassLoader());
		for(SipLoadBalancer  balancerDescription:new HashSet<SipLoadBalancer>(register.values())) {
			try {
				long startTime = System.currentTimeMillis();
				Registry registry = LocateRegistry.getRegistry(balancerDescription.getAddress().getHostAddress(), balancerDescription.getRmiPort());
				NodeRegisterRMIStub reg=(NodeRegisterRMIStub) registry.lookup("SIPBalancer");
                if(reachableCheck) {
                    ArrayList<SIPNode> reachableInfo = getReachableSIPNodeInfo(balancerDescription, info);
                    info = reachableInfo;
                    if(reachableInfo.isEmpty()) {
                        logger.logWarning("All connectors are unreachable from the balancer");
                    }
                }
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
				    logger.logTrace("Pinging the LB with the following Node Info [" + info + "]");
				}
				// https://github.com/RestComm/jain-sip.ha/issues/14
				// notify the listeners
				for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
					loadBalancerHeartBeatingListener.pingingloadBalancer(balancerDescription);
				}
				reg.handlePing(info);
				// notify the listeners
				for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
					loadBalancerHeartBeatingListener.pingedloadBalancer(balancerDescription);
				}
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
				    logger.logTrace("Pinged the LB with the following Node Info [" + info + "]");
				}
				balancerDescription.setDisplayWarning(true);
				if(!balancerDescription.isAvailable()) {
					logger.logInfo("Keepalive: SIP Load Balancer Found! " + balancerDescription);
				}
				balancerDescription.setAvailable(true);
				startTime = System.currentTimeMillis() - startTime;
				if(startTime>200)
					logger.logWarning("Heartbeat sent too slow in " + startTime + " millis at " + System.currentTimeMillis());

			} catch (IOException e) {
				balancerDescription.setAvailable(false);
				if(balancerDescription.isDisplayWarning()) {
					logger.logWarning("sendKeepAlive: Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
						"\nIf you need a cluster configuration make sure the SIP load balancer is running. Host " + balancerDescription.toString());
				}
				balancerDescription.setDisplayWarning(false);
			} catch (Exception e) {
				balancerDescription.setAvailable(false);
				if(balancerDescription.isDisplayWarning()) {
					logger.logError("sendKeepAlive: Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
						"\nIf you need a cluster configuration make sure the SIP load balancer is running. Host " + balancerDescription.toString(), e);
				}
				balancerDescription.setDisplayWarning(false);
			}
		}
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
			logger.logTrace("Finished gathering, Gathered info[" + info + "]");
		}
	}

	/**
	 * Contribution from Naoki Nishihara from OKI for Issue 1806 (SIP LB can not forward when node is listening on 0.0.0.0)
	 * Useful for a multi homed address, tries to reach a given load balancer from the list of ip addresses given in param
	 * @param balancerAddr the load balancer to try to reach
	 * @param info the list of node info from which we try to access the load balancer
	 * @return the list stripped from the nodes not able to reach the load balancer
	 */
	protected ArrayList<SIPNode> getReachableSIPNodeInfo(SipLoadBalancer balancer, ArrayList<SIPNode> info) {
		InetAddress balancerAddr = balancer.getAddress();
		if (balancerAddr.isLoopbackAddress()) {
			return info;
		}

		ArrayList<SIPNode> rv = new ArrayList<SIPNode>();
		for(SIPNode node: info) {
			if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
				logger.logTrace("Checking if " + node + " is reachable");
			}
			try {
				NetworkInterface ni = NetworkInterface.getByInetAddress(InetAddress.getByName(node.getIp()));
				// FIXME How can I determine the ttl?
				boolean b = balancerAddr.isReachable(ni, 5, 900);
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
					logger.logTrace(node + " is reachable ? " + b);
				}
				if(b) {
					if(balancer.getCustomInfo() != null && !balancer.getCustomInfo().isEmpty()) {
						for(Entry<Object, Object> entry : balancer.getCustomInfo().entrySet()) {
							if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
								logger.logDebug("Adding custom info with key " + (String)entry.getKey() + " and value " + (String)entry.getValue());
							}
							node.getProperties().put((String)entry.getKey(), (String)entry.getValue());
						}
					}
					rv.add(node);
				}
			} catch (IOException e) {
				logger.logError("IOException", e);
			}
		}

		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
			logger.logTrace("Reachable SIP Node:[balancer=" + balancerAddr + "],[node info=" + rv + "]");
		}

		return rv;
	}

	/**
	 * @param info
	 */
	protected void removeNodesFromBalancers() {
		ArrayList<SIPNode> info = new ArrayList<SIPNode>(sipNodes.values());

		Thread.currentThread().setContextClassLoader(NodeRegisterRMIStub.class.getClassLoader());
		for(SipLoadBalancer balancerDescription:new HashSet<SipLoadBalancer>(register.values())) {
			try {
				Registry registry = LocateRegistry.getRegistry(balancerDescription.getAddress().getHostAddress(),balancerDescription.getRmiPort());
				NodeRegisterRMIStub reg=(NodeRegisterRMIStub) registry.lookup("SIPBalancer");
				reg.forceRemoval(info);
				if(!balancerDescription.isAvailable()) {
					logger.logInfo("Remove: SIP Load Balancer Found! " + balancerDescription);
					balancerDescription.setDisplayWarning(true);
				}
				balancerDescription.setAvailable(true);
			} catch (IOException e) {
				if(balancerDescription.isDisplayWarning()) {
					logger.logWarning("remove: Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
							"\nIf you need a cluster configuration make sure the SIP load balancer is running.");
					balancerDescription.setDisplayWarning(false);
				}
				balancerDescription.setAvailable(true);
			} catch (Exception e) {
				if(balancerDescription.isDisplayWarning()) {
					logger.logError("remove: Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
							"\nIf you need a cluster configuration make sure the SIP load balancer is running.", e);
					balancerDescription.setDisplayWarning(false);
				}
				balancerDescription.setAvailable(true);
			}
		}
		if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
			logger.logTrace("Finished gathering, Gathered info[" + info + "]");
		}
	}

	/**
	 *
	 * @author Jean Deruelle
	 *
	 */
	protected class BalancerPingTimerTask extends TimerTask {

		@SuppressWarnings("unchecked")
		@Override
		public void run() {
			sendKeepAliveToBalancers();
		}
	}

	/**
	 * @param balancers the balancers to set
	 */
	public void setBalancers(String balancers) {
		this.balancers = balancers;
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#getJvmRoute()
	 */
	public String getJvmRoute() {
		return jvmRoute;
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#setJvmRoute(java.lang.String)
	 */
	public void setJvmRoute(String jvmRoute) {
		this.jvmRoute = jvmRoute;
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#addLoadBalancerHeartBeatingListener(org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingListener)
	 */
	public void addLoadBalancerHeartBeatingListener(
			LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener) {
		loadBalancerHeartBeatingListeners.add(loadBalancerHeartBeatingListener);
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#removeLoadBalancerHeartBeatingListener(org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingListener)
	 */
	public void removeLoadBalancerHeartBeatingListener(
			LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener) {
		loadBalancerHeartBeatingListeners.remove(loadBalancerHeartBeatingListener);
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#sendSwitchoverInstruction(org.mobicents.ha.javax.sip.SipLoadBalancer, java.lang.String, java.lang.String)
	 */
	public void sendSwitchoverInstruction(SipLoadBalancer sipLoadBalancer, String fromJvmRoute, String toJvmRoute) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)) {
			logger.logInfo("switching over from " + fromJvmRoute + " to " + toJvmRoute);
		}
		if(fromJvmRoute == null || toJvmRoute == null) {
			return;
		}
		ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();
		try {
			Thread.currentThread().setContextClassLoader(NodeRegisterRMIStub.class.getClassLoader());
			Registry registry = LocateRegistry.getRegistry(sipLoadBalancer.getAddress().getHostAddress(),sipLoadBalancer.getRmiPort());
			NodeRegisterRMIStub reg=(NodeRegisterRMIStub) registry.lookup("SIPBalancer");
			reg.switchover(fromJvmRoute, toJvmRoute);
			sipLoadBalancer.setDisplayWarning(true);
			if(logger.isLoggingEnabled(StackLogger.TRACE_INFO) && !sipLoadBalancer.isAvailable()) {
				logger.logInfo("Switchover: SIP Load Balancer Found! " + sipLoadBalancer);
			}
		} catch (IOException e) {
			sipLoadBalancer.setAvailable(false);
			if(sipLoadBalancer.isDisplayWarning()) {
				logger.logWarning("Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
				"\nIf you need a cluster configuration make sure the SIP load balancer is running.");
				sipLoadBalancer.setDisplayWarning(false);
			}
		} catch (Exception e) {
			sipLoadBalancer.setAvailable(false);
			if(sipLoadBalancer.isDisplayWarning()) {
				logger.logError("Cannot access the SIP load balancer RMI registry: " + e.getMessage() +
				"\nIf you need a cluster configuration make sure the SIP load balancer is running.", e);
				sipLoadBalancer.setDisplayWarning(false);
			}
		} finally {
			Thread.currentThread().setContextClassLoader(oldClassLoader);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.mobicents.ha.javax.sip.LoadBalancerHeartBeatingService#setGracefulShutdown(org.mobicents.ha.javax.sip.SipLoadBalancer, boolean)
	 */
	public void setGracefulShutdown(SipLoadBalancer sipLoadBalancer, boolean gracefullyShuttingDown) {
		this.gracefullyShuttingDown = gracefullyShuttingDown;
		updateConnectorsAsSIPNode();
		// forcing keep alive sending to update the nodes in the LB with the info
		// that the nodes are shutting down
		sendKeepAliveToBalancers();
	}

	public void setCustomInfo(SipLoadBalancer sipLoadBalancer) {
		updateConnectorsAsSIPNode();
	}

	public SipLoadBalancer[] getLoadBalancers() {
		// This is slow, but it is called rarely, so no prob
		return register.values().toArray(new SipLoadBalancer[] {});
	}

	public void addSipConnector(ListeningPoint listeningPoint) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)){
			logger.logInfo("Adding Listening Point to be using the Load Balancer for outbound traffic " + listeningPoint.getIPAddress() + ":" + listeningPoint.getPort() + "/" + listeningPoint.getTransport());
			logger.logInfo("Recomputing SIP Nodes to be sent to the LB");
		}
		// https://github.com/RestComm/jain-sip.ha/issues/3 we restrict only if one connector is passed forcefully this way
		useLoadBalancerForAllConnectors = false;
		sipConnectors.add(listeningPoint);
		updateConnectorsAsSIPNode();
	}

	public void addSipConnector(ListeningPoint listeningPoint, SipLoadBalancer loadBalancer) {
		if(loadBalancer != null) {
			throw new UnsupportedOperationException("This algorithm doesn't let you specify a load balancer per connector, please use instead " + MultiNetworkLoadBalancerHeartBeatingServiceImpl.class.getName());
		} else {
			addSipConnector(listeningPoint);
		}
	}

	public void removeSipConnector(ListeningPoint listeningPoint, SipLoadBalancer loadBalancer) {
		if(loadBalancer != null) {
			throw new UnsupportedOperationException("This algorithm doesn't let you specify a load balancer per connector, please use instead " + MultiNetworkLoadBalancerHeartBeatingServiceImpl.class.getName());
		} else {
			removeSipConnector(listeningPoint);
		}
	}

	public void removeSipConnector(ListeningPoint listeningPoint) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)){
			logger.logInfo("Removing Listening Point to be using the Load Balancer for outbound traffic " + listeningPoint.getIPAddress() + ":" + listeningPoint.getPort() + "/" + listeningPoint.getTransport());
			logger.logInfo("Recomputing SIP Nodes to be sent to the LB");
		}
		sipConnectors.remove(listeningPoint);
		updateConnectorsAsSIPNode();
	}
}


/*
 * Copyright 2013-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.ldif;

import com.btmatthews.maven.plugins.ldap.FormatWriter;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.util.ByteStringBuffer;

import java.io.IOException;
import java.io.OutputStream;

/**
 * This {@link FormatWriter} writes LDAP entries to an underlying output stream.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
public final class LDIFFormatWriter implements FormatWriter {

    /**
     * The underlying output stream.
     */
    private final OutputStream ldifOutputStream;
    /**
     * The system dependent end of line marker.
     */
    private final byte[] eol;
    /**
     * Used by the {@link #printEntry(com.unboundid.ldap.sdk.Entry)} method to determine if the
     * first entry has already been output.
     */
    private boolean first = true;

    /**
     * Initialize the {@link LDIFFormatWriter} by keeping a reference to the underlying output stream.
     *
     * @param outputStream The underlying output stream.
     */
    public LDIFFormatWriter(final OutputStream outputStream) {
        ldifOutputStream = outputStream;
        final String eolString = System.getProperty("line.separator", "\n");
        eol = eolString.getBytes();
    }

    /**
     * Write the LDAP entry to the underlying output stream in LDIF format.
     *
     * @param entry The directory entry.
     * @throws IOException If there was a problem writing to the underlying output stream.
     */
    public void printEntry(final Entry entry) throws IOException {
        if (entry != null) {
            final ByteStringBuffer buffer = new ByteStringBuffer();
            entry.toLDIF(buffer, 77);
            if (!first) {
                ldifOutputStream.write(eol);
            } else {
                first = false;
            }
            ldifOutputStream.write(buffer.toByteArray());
        }
    }

    /**
     * Close the {@link LDIFFormatWriter} which does not require any processing.
     */
    public void close() {
    }
}


/*
 * Copyright 2013-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap;

import com.unboundid.ldap.sdk.*;
import com.unboundid.ldif.LDIFChangeRecord;
import com.unboundid.ldif.LDIFException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

/**
 * Abstract base class for {@link FormatHandler} objects that import/export directory entries to/from LDAP directory
 * servers in LDIF and DSML format.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
public abstract class AbstractFormatHandler implements FormatHandler {

    /**
     * Reads directory entries from the input stream and loads them in the LDAP directory server.
     *
     * @param connection   The connection to the LDAP directory server.
     * @param inputStream  The input stream from which directory entries will be read.
     * @param ignoreErrors If {@code true} then loading will continue if an error occurs.
     * @param logger       Used to log information or error messages.
     */
    public final void load(final LDAPInterface connection,
                           final InputStream inputStream,
                           final boolean ignoreErrors,
                           final FormatLogger logger) {
        final FormatReader reader = openReader(inputStream, logger);
        if (reader != null) {
            try {
                boolean keepReading = true;
                do {
                    try {
                        final LDIFChangeRecord record = reader.nextRecord();
                        if (record == null) {
                            keepReading = false;
                        } else {
                            record.processChange(connection);
                        }
                    } catch (final LDIFException e) {
                        if (!ignoreErrors || !e.mayContinueReading()) {
                            logger.logError("Error parsing directory entry read from the input stream", e);
                            keepReading = false;
                        }
                    } catch (final LDAPException e) {
                        if (!ignoreErrors) {
                            logger.logError("Error loading directory entry into the LDAP directory server", e);
                            keepReading = false;
                        }
                    }
                } while (keepReading);
            } catch (final IOException e) {
                logger.logError("I/O error reading directory entry from input stream", e);
            } finally {
                try {
                    reader.close();
                } catch (final IOException e) {
                    logger.logError("I/O error closing the input stream reader", e);
                }
            }
        }
    }

    /**
     * Dump the results of a search against the LDAP directory server to an output stream.
     *
     * @param connection   The connection to the LDAP directory server.
     * @param base         The base DN from which to start the search.
     * @param filter       Query used to filter the directory entries.
     * @param outputStream The output stream to which the directory entries are to be written.
     * @param logger       Used to log information or error messages.
     */
    public final void dump(final LDAPInterface connection,
                           final String base,
                           final String filter,
                           final OutputStream outputStream,
                           final FormatLogger logger) {
        final FormatWriter ldapWriter = createWriter(outputStream, logger);
        if (ldapWriter == null) {
            logger.logError("Error creating writer for output stream");
        } else {
            try {
                try {
                    final SearchRequest request = new SearchRequest(base, SearchScope.SUB, Filter.create(filter));
                    final SearchResult result = connection.search(request);
                    if (result.getResultCode() == ResultCode.SUCCESS) {
                        final List<SearchResultEntry> entries = result.getSearchEntries();
                        if (entries != null) {
                            for (final SearchResultEntry entry : entries) {
                                ldapWriter.printEntry(entry);
                            }
                        } else {
                            logger.logInfo("Search did not return any directory entries");
                        }
                    } else {
                        logger.logError("Search operation failed");
                    }
                } catch (final LDAPException e) {
                    logger.logError("Error searching the LDAP directory", e);
                } finally {
                    ldapWriter.close();
                }
            } catch (final IOException e) {
                logger.logError("Error writing directory entry to the output stream", e);
            }
        }
    }

    /**
     * Create a writer that formats the directory entry before writing it to the output stream.
     *
     * @param outputStream The target output stream.
     * @param logger       Used to log information or error messages.
     * @return A {@link FormatWriter} object.
     */
    protected abstract FormatWriter createWriter(OutputStream outputStream, FormatLogger logger);

    /**
     * Create a reader to parse the directory entries as they are read from the input stream.
     *
     * @param inputStream The source input stream.
     * @param logger      Used to log information or error messages.
     * @return A {@link FormatReader} object.
     */
    protected abstract FormatReader openReader(InputStream inputStream, FormatLogger logger);
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.mojo;

import com.btmatthews.maven.plugins.ldap.FormatHandler;
import com.btmatthews.maven.plugins.ldap.dsml.DSMLFormatHandler;
import com.btmatthews.maven.plugins.ldap.ldif.LDIFFormatHandler;
import com.unboundid.ldap.sdk.LDAPConnection;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.*;

/**
 * This is the abstract base class for all Mojos in the ldap-maven-plugin plugin
 * that dump content from the directory server. Concrete classes must implement
 * the getLDAPWriter() method to return the writer that will format the output
 * appropriately.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
@Mojo(name = "dump")
public final class DumpMojo extends AbstractLDAPMojo {
    /**
     * Handler used to dump LDAP directory entries to DSML files.
     */
    private final FormatHandler dsmlFormatHandler = new DSMLFormatHandler();
    /**
     * Handler used to dump LDAP directory entries to LDIF files.
     */
    private final FormatHandler ldifFormatHandler = new LDIFFormatHandler();
    /**
     * The search base.
     */
    @Parameter(required = true)
    private String searchBase;
    /**
     * The search filter.
     */
    @Parameter(defaultValue = "(objectclass=*)", required = true)
    private String searchFilter;
    /**
     * The target output directory.
     */
    @Parameter(defaultValue = "${project.build.directory}", required = true)
    private File outputDirectory;
    /**
     * The output file name.
     */
    @Parameter(required = true)
    private String filename;
    /**
     * The output file format.
     *

     *
ldif

     *
dsml

     *

     */
    @Parameter(defaultValue = "ldif")
    private String format;

    /**
     * Execute the plugin goal by dumping the matching directory entries to a file in the specified format.
     *
     * @throws MojoExecutionException If something unexpected happens.
     */
    public final void execute() throws MojoExecutionException {
        final File outputFile = new File(outputDirectory, filename);
        if (outputDirectory.exists() || outputDirectory.mkdirs()) {
            try {
                final OutputStream outputStream = new FileOutputStream(outputFile);
                try {
                    final LDAPConnection connection = connect();
                    try {
                        final FormatHandler handler = getFormatHandler();
                        if (handler != null) {
                            handler.dump(connection, searchBase, searchFilter, outputStream, this);
                        }
                    } finally {
                        connection.close();
                    }
                } finally {
                    try {
                        outputStream.close();
                    } catch (final IOException e) {
                    }
                }
            } catch (final FileNotFoundException e) {
            }
        }
    }

    /**
     * Get the appropriate format handler based on the output file format.
     *
     * @return The appropriate file handler or {@code null} if the output file format is not supported.
     */
    private FormatHandler getFormatHandler() {
        if (format.equals("dsml")) {
            return dsmlFormatHandler;
        } else if (format.equals("ldif")) {
            return ldifFormatHandler;
        } else {
            return null;
        }
    }
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.mojo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

/**
 * The abstract base class for DSML or LDIF source files located on the class path or file system.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
public abstract class AbstractSource implements Source {

    /**
     * The prefix for class path resources.
     */
    private static final String CLASSPATH_PREFIX = "classpath:";
    /**
     * The length of the prefix for class path resources.
     */
    private static final int CLASSPATH_PREFIX_LENGTH = 10;
    /**
     * The location of the source on the class path or file system.
     */
    private String path;

    /**
     * Initialise the source setting the location of the source on the class path or file system.
     *
     * @param path The source location.
     */
    protected AbstractSource(final String path) {
        this.path = path;
    }

    /**
     * Method call by plexus to inject path after construction of the class.
     *
     * @param path The path of the source file.
     */
    public void set(String path) {
        this.path = path;
    }

    /**
     * Open the resource or file and return the input stream. If the path is prefixed with classpath: then it is treated
     * as a classpath resource. Otherwise, it is assumed to be a file system path.
     *
     * @return The input stream or {@code null} if the file or resource cannot be found.
     */
    public InputStream open() {
        if (path.startsWith(CLASSPATH_PREFIX)) {
            final ClassLoader loader = Thread.currentThread().getContextClassLoader();
            return loader.getResourceAsStream(path.substring(CLASSPATH_PREFIX_LENGTH));
        } else {
            final File file = new File(path);
            try {
                return new FileInputStream(file);
            } catch (final FileNotFoundException e) {
                return null;
            }
        }
    }

    /**
     * Convert the source to a string.
     *
     * @return Returns the value of {@link #path}.
     */
    @Override
    public final String toString() {
        return path;
    }
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.mojo;

import com.btmatthews.maven.plugins.ldap.FormatLogger;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;

/**
 * This is the abstract base class for all Mojos in this the ldap-maven-plugin
 * plugin. It defines the properties for the LDAP directory server connection
 * and provides a method to connect to the LDAP directory server.
 *
 * @author Brian Matthews
 * @since 1.0.0
 */
public abstract class AbstractLDAPMojo extends AbstractMojo implements FormatLogger {
    /**
     * The default host name.
     */
    private static final String DEFAULT_HOST = "localhost";
    /**
     * The default port number for LDAP servers.
     */
    private static final int DEFAULT_PORT = 389;
    /**
     * The host name of the LDAP directory server. Defaults to localhost.
     */
    @Parameter(defaultValue = "localhost")
    private String host = AbstractLDAPMojo.DEFAULT_HOST;
    /**
     * The port number of the LDAP directory server. Defaults to 389.
     */
    @Parameter(defaultValue = "389")
    private int port = AbstractLDAPMojo.DEFAULT_PORT;
    /**
     * The distinguished name used if authentication is required.
     */
    @Parameter(required = true)
    private String authDn;
    /**
     * The password used if authentication is required.
     */
    @Parameter(required = true)
    private String passwd;
    /**
     * The connection timeout.
     */
    @Parameter(defaultValue = "5000")
    private int connectionTimeout = 5000;
    /**
     * The maximum number of connection attempts before failing.
     */
    @Parameter(defaultValue = "3")
    private int connectionRetries = 3;
    /**
     * To skip execution of the plugin
     */
    @Parameter(defaultValue = "false")
    private boolean skip;

    /**
     * Connect to the LDAP directory server. The connection attempt will be retried {@link #connectionRetries} times
     * and the connection time is set to {@link #connectionTimeout}.
     *
     * @return The connection object.
     * @throws MojoExecutionException If the connection to the LDAP directory server failed.
     */
    protected final LDAPConnection connect() throws MojoExecutionException {
        final LDAPConnection connection = new LDAPConnection();
        int i = 0;
        while (i < connectionRetries) {
            long start = System.currentTimeMillis();
            try {
                this.getLog().info("Attempting to connect ot LDAP directory server (" + host + ":" + port + ")");
                connection.connect(host, port, connectionTimeout);
                break;
            } catch (final LDAPException e) {
                final String message = "Could not connect to LDAP directory server (" + host + ":" + port + ")";
                this.getLog().error(message, e);
                if (i++ < connectionRetries) {
                    long time = System.currentTimeMillis() - start;
                    if (time < connectionTimeout) {
                        try {
                            Thread.sleep(connectionTimeout - time);
                        } catch (final InterruptedException e1) {
                            throw new MojoExecutionException(message, e1);
                        }
                    }
                } else {
                    throw new MojoExecutionException(message, e);
                }
            }
        }
        try {
            connection.bind(authDn, passwd);
        } catch (final LDAPException e) {
            throw new MojoExecutionException("Could not bind to LDAP directory server as " + authDn, e);
        }
        return connection;
    }

    public boolean isSkip(){
        return this.skip;
    }

    /**
     * Write a information message to the Maven log.
     *
     * @param message The information message.
     */
    public void logInfo(final String message) {
        getLog().info(message);
    }

    /**
     * Write an error message to the Maven log.
     *
     * @param message The error message.
     */
    public void logError(final String message) {
        getLog().error(message);
    }

    /**
     * Write an error message and stack trace to the Maven log.
     *
     * @param message   The error message.
     * @param exception The exception containing the stack trace.
     */
    public void logError(final String message, final Throwable exception) {
        getLog().error(message, exception);
    }
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.mojo;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.InvalidRepositoryException;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.DefaultRepositoryRequest;
import org.apache.maven.artifact.repository.RepositoryRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.component.configurator.AbstractComponentConfigurator;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.configurator.ConfigurationListener;
import org.codehaus.plexus.component.configurator.converters.composite.ObjectWithFieldsConverter;
import org.codehaus.plexus.component.configurator.converters.special.ClassRealmConverter;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.configuration.PlexusConfiguration;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.*;
import java.util.regex.Pattern;

/**
 * @author Brian Matthews
 * @since 1.2.0
 */
@Component(role = ComponentConfigurator.class, hint = "include-server-dependencies")
public class IncludeServerDependenciesComponentConfigurator extends AbstractComponentConfigurator {

    /**
     * The plugin configuration element that specifies the LDAP server type.
     */
    private static final String SERVER_TYPE_ATTRIBUTE = "serverType";
    /**
     * The default LDAP server type.
     */
    private static final String DEFAULT_SERVER_TYPE_VALUE = "unboundid";
    /**
     * The group id of the main artifact for the LDAP server type.
     */
    private static final String DEFAULT_GROUP_ID = "com.btmatthews.maven.plugins.ldap";
    /**
     * Used to construct the artifact id of the main artifact for the LDAP server type.
     */
    private static final String DEFAULT_ARTIFACT_ID_FORMAT = "server-{0}";

    @Requirement
    private RepositorySystem repositorySystem;

    /**
     * Configure the Mojo by adding the dependencies for the LDAP server type to the class loader.
     *
     * @param component           The Mojo being configured.
     * @param configuration       The plugin configuration.
     * @param expressionEvaluator Used to evaluate expressions.
     * @param containerRealm      Used to build the class loader.
     * @param listener            Receives notification when configuration is changed.
     * @throws ComponentConfigurationException If there was a problem resolving or locating any of the dependencies.
     */
    @Override
    public void configureComponent(final Object component,
                                   final PlexusConfiguration configuration,
                                   final ExpressionEvaluator expressionEvaluator,
                                   final ClassRealm containerRealm,
                                   final ConfigurationListener listener)
            throws ComponentConfigurationException {

        final String serverType = getServerType(configuration);

        if (!serverType.startsWith("dependency-")) {
            addServerDependenciesToClassRealm(serverType, expressionEvaluator, containerRealm);
        }

        converterLookup.registerConverter(new ClassRealmConverter(containerRealm));

        final ObjectWithFieldsConverter converter = new ObjectWithFieldsConverter();

        converter.processConfiguration(converterLookup, component, containerRealm, configuration,
                expressionEvaluator, listener);
    }

    /**
     * Resolve the dependencies for an LDAP server type and add them to the class loader.
     *
     * @param serverType          The LDAP server type.
     * @param expressionEvaluator Used to evaluate expressions.
     * @param containerRealm      The class loader.
     * @throws ComponentConfigurationException If there was a problem resolving or locating any of the dependencies.
     */
    private void addServerDependenciesToClassRealm(final String serverType,
                                                   final ExpressionEvaluator expressionEvaluator,
                                                   final ClassRealm containerRealm)
            throws ComponentConfigurationException {
        final Collection<Artifact> classpathElements = getServerDependencies(serverType, expressionEvaluator);
        if (classpathElements != null) {
            for (final URL url : buildURLs(classpathElements)) {
                containerRealm.addURL(url);
            }
        }
    }

    /**
     * Retrive the URLs used to locate the JAR files for a list of artifacts.
     *
     * @param classpathElements The list of artifacts.
     * @return The list of URLs.
     * @throws ComponentConfigurationException If any JAR files could not be located.
     */
    private List<URL> buildURLs(final Collection<Artifact> classpathElements)
            throws ComponentConfigurationException {
        final List<URL> urls = new ArrayList<URL>(classpathElements.size());
        for (final Artifact classpathElement : classpathElements) {
            try {
                final URL url = classpathElement.getFile().toURI().toURL();
                urls.add(url);
            } catch (final MalformedURLException e) {
                throw new ComponentConfigurationException("Unable to access project dependency: " + classpathElement, e);
            }
        }
        return urls;
    }

    /**
     * Resolve the LDAP server type artifact and its dependencies.
     *
     * @param serverType          The LDAP server type.
     * @param expressionEvaluator Used to get the Maven project model and repository session objects.
     * @return A list of dependencies required for the LDAP server type.
     * @throws ComponentConfigurationException If the was a problem resolving the dependencies for
     *                                         the LDAP server type.
     */
    private Collection<Artifact> getServerDependencies(final String serverType,
                                                       final ExpressionEvaluator expressionEvaluator)
            throws ComponentConfigurationException {
        try {
            final MavenProject project = (MavenProject) expressionEvaluator.evaluate("${project}");
            final String localRepo = (String) expressionEvaluator.evaluate("${settings.localRepository}");
            final ArtifactRepository localRepository = repositorySystem.createLocalRepository(new File(localRepo));
            final RepositoryRequest repositoryRequest = new DefaultRepositoryRequest();
            repositoryRequest.setRemoteRepositories(project.getRemoteArtifactRepositories());
            repositoryRequest.setLocalRepository(localRepository);
            final ArtifactResolutionRequest request = new ArtifactResolutionRequest(repositoryRequest);
            request.setArtifact(getServerArtifact(serverType));
            request.setResolveTransitively(true);
            final ArtifactResolutionResult result = repositorySystem.resolve(request);
            if (result.isSuccess()) {
                return result.getArtifacts();
            }
            boolean first = true;
            final StringBuilder builder = new StringBuilder("Cannot resolve dependencies: [");
            for (final Artifact artifact : result.getMissingArtifacts()) {
                if (!first) {
                    builder.append(',');
                } else {
                    first = false;
                }
                builder.append(artifact.getGroupId());
                builder.append(':');
                builder.append(artifact.getArtifactId());
                builder.append(':');
                builder.append(artifact.getVersion());
            }
            builder.append("]");
            throw new ComponentConfigurationException(builder.toString());
        } catch (final ExpressionEvaluationException e) {
            throw new ComponentConfigurationException("Error evaluating expression", e);
        } catch (final InvalidRepositoryException e) {
            throw new ComponentConfigurationException("Error resolving local repository", e);
        }
    }

    /**
     * Get the artifact descriptor for the JAR file that implements support for the LDAP server type.
     *
     * @param serverType The LDAP server type.
     * @return The JAR file artifact descriptor.
     */
    private Artifact getServerArtifact(final String serverType)
            throws ComponentConfigurationException {
        if (serverType.startsWith("gav-")) {
            int index = serverType.indexOf("-", 4);
            if (index > 0) {
                String[] gav = serverType.substring(index + 1).split(":");
                if (gav.length == 3) {
                    return repositorySystem.createArtifact(
                            gav[0],
                            gav[1],
                            gav[2],
                            "runtime",
                            "jar"
                    );
                }
            }
            throw new ComponentConfigurationException("Invalid server type: " + serverType);
        } else {
            return repositorySystem.createArtifact(
                    DEFAULT_GROUP_ID,
                    MessageFormat.format(DEFAULT_ARTIFACT_ID_FORMAT, serverType),
                    getClass().getPackage().getImplementationVersion(),
                    "runtime",
                    "jar");
        }
    }

    /**
     * Determine the configured LDAP server type. If one has not been configured then
     * return the default.
     *
     * @param configuration The plugin configuration.
     * @return The LDAP server type.
     */
    private String getServerType(final PlexusConfiguration configuration) {
        final Pattern pattern = Pattern.compile("\\$\\{[A-Za-z0-9\\._]+\\}");
        for (final PlexusConfiguration cfg : configuration.getChildren()) {
            if (cfg.getName().equals(SERVER_TYPE_ATTRIBUTE)) {
                if (pattern.matcher(cfg.getValue()).matches()) {
                    return DEFAULT_SERVER_TYPE_VALUE;
                } else {
                    return cfg.getValue();
                }
            }
        }
        return DEFAULT_SERVER_TYPE_VALUE;
    }
}


/*
 * Copyright 2013-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.dsml;

import com.btmatthews.maven.plugins.ldap.AbstractFormatHandler;
import com.btmatthews.maven.plugins.ldap.FormatLogger;
import com.btmatthews.maven.plugins.ldap.FormatReader;
import com.btmatthews.maven.plugins.ldap.FormatWriter;
import org.dom4j.DocumentException;
import org.jaxen.JaxenException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * A {@link com.btmatthews.maven.plugins.ldap.FormatHandler} that imports/exports LDAP directory entries from/to
 * DSML formatted files.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
public final class DSMLFormatHandler extends AbstractFormatHandler {
    /**
     * Create the LDAP writer that will dump LDAP entries to a DSML file.
     *
     * @param outputStream The target output stream.
     * @param logger       Used to log information or error messages.
     * @return A {@link DSMLFormatWriter} object.
     */
    @Override
    protected FormatWriter createWriter(final OutputStream outputStream,
                                        final FormatLogger logger) {
        try {
            return new DSMLFormatWriter(outputStream);
        } catch (final IOException e) {
            logger.logError("Could not create and intialise the DSML writer", e);
        }
        return null;
    }

    /**
     * Create the LDAP reader that will load LDAP entries from a DSML file.
     *
     * @param inputStream The file input stream.
     * @param logger      Used to log information or error messages.
     * @return A {@link DSMLFormatReader} object.
     */
    @Override
    protected FormatReader openReader(final InputStream inputStream,
                                      final FormatLogger logger) {
        try {
            return new DSMLFormatReader(inputStream);
        } catch (final DocumentException e) {
            logger.logError("Error parsing DSML file", e);
        } catch (final JaxenException e) {
            logger.logError("Error processing DSML file", e);
        } catch (final IOException e) {
            logger.logError("Error reading DSML file", e);
        }
        return null;
    }
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap;

import com.btmatthews.utils.monitor.AbstractServer;
import com.btmatthews.utils.monitor.Logger;

import java.io.File;

/**
 * Abstract base class for {@link LDAPServer} implementations.
 *
 * @author Brian Matthews
 * @since 1.1.1
 */
public abstract class AbstractLDAPServer extends AbstractServer implements LDAPServer {

    /**
     * The root DN of the LDAP directory.
     */
    private String root;
    /**
     * The object classes used for the root DN.
     */
    private String[] objectClasses;
    /**
     * The distinguished name of the admin account.
     */
    private String authDn;
    /**
     * The password for the admin account.
     */
    private String passwd;
    /**
     * The working directory used by the LDAP directory service to store directory data.
     */
    private File workingDirectory;
    /**
     * The LDIF file used to seed the LDAP directory.
     */
    private File ldifFile;
    /**
     * The TCP port on which the server is listening for LDAP traffic.
     */
    private int serverPort;

    /**
     * Used to configure the root DN of the LDAP directory, the working directory used by the directory service to
     * store the directory data, the LDIF file used to seed the directory or the TCP port number on which the server
     * will listening for LDAP traffic.
     *
     * @param name   The name of the property to configure.
     * @param value  The value of the property being configured.
     * @param logger Used to log error and information messages.
     */
    @Override
    public void configure(final String name, final Object value, final Logger logger) {
        if (ROOT.equals(name)) {
            if (value instanceof String) {
                root = (String) value;
                logger.logInfo("Configured root DN for directory server: " + root);
            }
        } else if (OBJECT_CLASSES.equals(name)) {
            if (value instanceof String[]) {
                objectClasses = (String[]) value;
                logger.logInfo("Configured object classes for root DN: " + objectClasses);
            }
        } else if (AUTH_DN.equals(name)) {
            if (value instanceof String) {
                authDn = (String) value;
                logger.logInfo("Configured admin identity for directory server: " + authDn);
            }
        } else if (PASSWD.equals(name)) {
            if (value instanceof String) {
                passwd = (String) value;
                logger.logInfo("Configured admin credentials for directory server: " + passwd);
            }
        } else if (WORK_DIR.equals(name)) {
            if (value instanceof File) {
                workingDirectory = (File) value;
                logger.logInfo("Configured working directory for directory server: " + workingDirectory);
            }
        } else if (LDIF_FILE.equals(name)) {
            if (value instanceof File) {
                ldifFile = (File) value;
                logger.logInfo("Configured LDIF seed data source for directory server: " + ldifFile);
            }
        } else if (LDAP_PORT.equals(name)) {
            if (value instanceof Integer) {
                serverPort = (Integer) value;
                logger.logInfo("Configured TCP port for directory server: " + serverPort);
            }
        }
    }

    /**
     * Get the configured directory root.
     *
     * @return The directory root.
     */
    public final String getRoot() {
        return root;
    }

    /**
     * Get the object classes used to create the directory root.
     *
     * @return The object classes.
     */
    public final String[] getObjectClasses() {
        return objectClasses;
    }

    /**
     * Get the distinguished name of the admin account.
     *
     * @return The distinguished name of the admin account.
     */
    public final String getAuthDn() {
        return authDn;
    }

    /**
     * Get the password for the admin account.
     *
     * @return The password for the admin account.
     */
    public final String getPasswd() {
        return passwd;
    }

    /**
     * Get the working directory used to create temporary files.
     *
     * @return The working directory.
     */
    public final File getWorkingDirectory() {
        return workingDirectory;
    }

    /**
     * Get the LDIF file that contains the data used to seed the directory.
     *
     * @return The LDIF file.
     */
    public final File getLdifFile() {
        return ldifFile;
    }

    /**
     * Get the port on which the LDAP server is listening.
     *
     * @return The port.
     */
    public final int getServerPort() {
        return serverPort;
    }
}


/*
 * Copyright 2013-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.unboundid;

import com.btmatthews.maven.plugins.ldap.AbstractLDAPServer;
import com.btmatthews.utils.monitor.Logger;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldif.LDIFReader;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Implements an in-memory LDAP directory server using the
 * UnboundID SDK.
 *
 * @author Brian Matthews
 * @since 1.1.1
 */
public final class UnboundIDServer extends AbstractLDAPServer {

    /**
     * The default object classes used for the root DN entry.
     */
    private static final String[] DEFAULT_ROOT_OBJECT_CLASSES = { "domain", "top" };

    /**
     * The in-memory instance of the UnboundID directory server.
     */
    private InMemoryDirectoryServer server;

    /**
     * Configure and start the embedded UnboundID server creating the root DN and loading the LDIF seed data.
     *
     * @param logger Used to log informational and error messages.
     */
    @Override
    public void start(final Logger logger) {
        try {
            logger.logInfo("Starting UnboundID server");
            final InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("default", getServerPort());
            final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(new DN(getRoot()));
            config.setListenerConfigs(listenerConfig);
            if (getAuthDn() != null) {
                config.addAdditionalBindCredentials(getAuthDn(), getPasswd());
            }
            server = new InMemoryDirectoryServer(config);
            String[] objectClasses = getObjectClasses();
            if (objectClasses == null) {
                objectClasses = DEFAULT_ROOT_OBJECT_CLASSES;
            }
            server.add(new Entry(getRoot(), new Attribute("objectclass", objectClasses)));
            if (getLdifFile() != null) {
                final InputStream in = new FileInputStream(getLdifFile());
                try {
                    final LDIFReader reader = new LDIFReader(in);
                    server.importFromLDIF(false, reader);
                } finally {
                    in.close();
                }
            }
            server.startListening();
            logger.logInfo("Started UnboundID server");
        } catch (final LDAPException e) {
            e.printStackTrace();
            logger.logError("Could not launch embedded UnboundID directory server", e);
        } catch (final IOException e) {
            e.printStackTrace();
            logger.logError("Could not launch embedded UnboundID directory server", e);
        }
    }

    /**
     * Shutdown the the embedded UnboundID server.
     *
     * @param logger Used to log informational and error messages.
     */
    @Override
    public void stop(final Logger logger) {
        logger.logInfo("Stopping UnboundID server");
        server.shutDown(true);
        logger.logInfo("Stopped UnboundID server");
    }
}


/*
 * Copyright 2013-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.dsml;

import com.btmatthews.maven.plugins.ldap.FormatReader;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldif.LDIFAddChangeRecord;
import com.unboundid.ldif.LDIFChangeRecord;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.jaxen.JaxenException;
import org.jaxen.NamespaceContext;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.XPath;
import org.jaxen.dom4j.Dom4jXPath;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * A {@link FormatReader} that reads LDAP directory entries from a DSML file.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
public final class DSMLFormatReader implements FormatReader {

    /**
     * Iterates over the directory entries extracted from the .dsml file.
     */
    private final Iterator<Node> entryIterator;
    /**
     * The namespace context maps the dsml prefix to the http://www.dsml.org/DSML namespace.
     */
    private final NamespaceContext namespaceContext;
    /**
     * The {@link XPath} expression used to iterate through the object classes for the DSML entry.
     */
    private final XPath objectClassXPath;
    /**
     * The {@link XPath} expression used to iterate through the attribute for the DSML entry.
     */
    private final XPath attrXPath;
    /**
     * The {@link XPath} expression used to iterate through the values of each attribute for the DSML entry.
     */
    private final XPath attrValueXPath;

    /**
     * Initialise the reader to read DSML entries from an underlying input stream.
     *
     * @param inputStream The underlying input stream.
     * @throws DocumentException If there was a problem parsing the DSML file.
     * @throws JaxenException    If there was a problem creating the {@link XPath} expressions.
     * @throws IOException       If there was a problem reading the DSML file.
     */
    public DSMLFormatReader(final InputStream inputStream) throws DocumentException, IOException, JaxenException {
        final Map<String, String> map = new HashMap<String, String>();
        map.put("dsml", "http://www.dsml.org/DSML");
        namespaceContext = new SimpleNamespaceContext(map);
        final SAXReader reader = new SAXReader();
        final Document document = reader.read(inputStream);
        final XPath xpath = createXPath("/dsml[namespace-uri()='http://www.dsml.org/DSML']/dsml:directory-entries/dsml:entry");
        objectClassXPath = createXPath("dsml:objectclass/dsml:oc-value");
        attrXPath = createXPath("dsml:attr");
        attrValueXPath = createXPath("dsml:value");
        final List<Node> entries = (List<Node>) xpath.selectNodes(document);
        entryIterator = entries.iterator();
    }

    /**
     * Read the next change record from the underlying input stream.
     *
     * @return The next change record or {@code null} if the end of the input stream has been reached.
     */
    public LDIFChangeRecord nextRecord() {
        if (entryIterator.hasNext()) {
            try {
                final Node entryNode = entryIterator.next();
                final String dn = entryNode.valueOf("@dn");
                final List<Attribute> attributes = new ArrayList<Attribute>();

                final List<Node> objectClassList = (List<Node>) objectClassXPath.selectNodes(entryNode);
                final String[] objectClasses = new String[objectClassList.size()];
                for (int j = 0; j < objectClasses.length; ++j) {
                    objectClasses[j] = objectClassList.get(j).getStringValue();
                }
                attributes.add(new Attribute("objectclass", objectClasses));
                for (final Node attributeNode : (List<Node>) attrXPath.selectNodes(entryNode)) {
                    final String attributeName = attributeNode.valueOf("@name");
                    final List<Node> attributeValueNodes = (List<Node>) attrValueXPath.selectNodes(attributeNode);
                    switch (attributeValueNodes.size()) {
                        case 0:
                            break;
                        case 1: {
                            final String attributeValue = attributeValueNodes.get(0).getStringValue();
                            attributes.add(new Attribute(attributeName, attributeValue));
                            break;
                        }
                        default: {
                            final String[] attributeValues = new String[attributeValueNodes.size()];
                            for (int j = 0; j < attributeValueNodes.size(); ++j) {
                                attributeValues[j] = attributeValueNodes.get(j).getStringValue();
                            }
                            attributes.add(new Attribute(attributeName, attributeValues));
                            break;
                        }
                    }
                }
                return new LDIFAddChangeRecord(dn, attributes);
            } catch (final JaxenException e) {
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Called to close {@link DSMLFormatReader}.
     */
    public void close() {
    }

    /**
     * Create a {@link XPath} for the expression {@code xpathString}.
     *
     * @param xpathString The expression.
     * @return The {@link XPath}.
     * @throws JaxenException If there was a problem parsing the {@code xpathString} expression.
     */
    private XPath createXPath(final String xpathString) throws JaxenException {
        final XPath xpath = new Dom4jXPath(xpathString);
        xpath.setNamespaceContext(namespaceContext);
        return xpath;
    }
}


/*
 * Copyright 2008-2016 Brian Thomas Matthews
 *
 * 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.
 */

package com.btmatthews.maven.plugins.ldap.mojo;

import com.btmatthews.maven.plugins.ldap.FormatHandler;
import com.btmatthews.maven.plugins.ldap.dsml.DSMLFormatHandler;
import com.btmatthews.maven.plugins.ldap.ldif.LDIFFormatHandler;
import com.unboundid.ldap.sdk.LDAPConnection;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.IOException;
import java.io.InputStream;

/**
 * Implement the goal that loads a LDIF or DSML file into the LDAP directory server.
 *
 * @author Brian Matthews
 * @since 1.2.0
 */
@Mojo(name = "load")
public final class LoadMojo extends AbstractLDAPMojo {

    /**
     * Handler used to load LDAP directory entries from DSML files.
     */
    private final FormatHandler dsmlFormatHandler = new DSMLFormatHandler();
    /**
     * Handler used to load LDAP directory entries from LDIF files.
     */
    private final FormatHandler ldifFormatHandler = new LDIFFormatHandler();
    /**
     * The LDIF and DSML files to be processed.
     */
    @SuppressWarnings("MismatchedReadAndWriteOfArray")
    @Parameter(required = true)
    private Source[] sources;
    /**
     * Indicates if the plugin should continue if there is an error. The default
     * is to halt on error.
     */
    @Parameter(defaultValue = "false")
    private boolean continueOnError;

    /**
     * Execute the plugin goal iterating over the list of source files and loading the LDAP directory entries from
     * each file using the appropriate handler.
     *
     * @throws MojoExecutionException If there was an error executing the plugin goal.
     */
    public void execute() throws MojoExecutionException {
        if (!isSkip()){
            final LDAPConnection connection = connect();
            try {
                for (final Source source : sources) {
                    try {
                        getLog().info("Processing input source: " + source);
                        final FormatHandler handler = getFormatHandler(source);
                        if (handler == null) {
                            getLog().warn("No handler for input source: " + source);
                        } else {
                            final InputStream inputStream = source.open();
                            if (inputStream == null) {
                                if (!this.continueOnError) {
                                    throw new MojoExecutionException("Cannot open source for reading: " + source);
                                } else {
                                    getLog().warn("Skipping source that could not be opened for reading: " + source);
                                }
                            } else {
                                try {
                                    handler.load(connection, source.open(), continueOnError, this);
                                } finally {
                                    inputStream.close();
                                }
                            }
                        }
                    } catch (final IOException e) {
                        if (!this.continueOnError) {
                            throw new MojoExecutionException("Error closing input source: " + source, e);
                        } else {
                            this.getLog().warn("Ignoring error closing input source: " + source, e);
                        }
                    }
                }
            } finally {
                connection.close();
            }
        }
    }

    /**
     * Determine which format handler to use for a source file. If the source file is DSML then {@link #dsmlFormatHandler}
     * will be used and if it is LDIF then {@link #ldifFormatHandler}.
     *
     * @param source Describes the source file.
     * @return The appropriate format handler or {@code null} if the source is not supported.
     */
    private FormatHandler getFormatHandler(final Source source) {
        if (source instanceof Dsml) {
            return dsmlFormatHandler;
        } else if (source instanceof Ldif) {
            return ldifFormatHandler;
        } else {
            return null;
        }
    }
}