/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package gov.nist.javax.sip.stack;

import gov.nist.core.CommonLogger;
import gov.nist.core.StackLogger;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.header.Contact;
import gov.nist.javax.sip.header.Route;
import gov.nist.javax.sip.header.RouteList;
import gov.nist.javax.sip.header.SIPHeader;
import gov.nist.javax.sip.message.SIPResponse;
import gov.nist.javax.sip.parser.EventParser;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import javax.sip.DialogState;
import javax.sip.PeerUnavailableException;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.address.Address;
import javax.sip.address.AddressFactory;
import javax.sip.header.ContactHeader;
import javax.sip.header.EventHeader;
import javax.sip.header.HeaderFactory;

import org.mobicents.ha.javax.sip.ClusteredSipStack;
import org.mobicents.ha.javax.sip.HASipDialog;
import org.mobicents.ha.javax.sip.ReplicationStrategy;

/**
 * @author jean.deruelle@gmail.com
 *
 */
public abstract class AbstractHASipDialog extends SIPDialog implements HASipDialog {
	private static StackLogger logger = CommonLogger.getLogger(AbstractHASipDialog.class);
	private static final long serialVersionUID = 1L;
	public static final String B2BUA = "b2b";
	public static final String EVENT_HEADER = "eh";
	public static final String REMOTE_TARGET = "rt";
	public static final String TERMINATE_ON_BYE = "tob";
	public static final String ROUTE_LIST = "rl";
	public static final String IS_REINVITE = "ir";
	public static final String LAST_RESPONSE = "lr";
	public static final String IS_SERVER = "is";
	public static final String IS_LATEST_TX_SERVER = "ilts";
	public static final String FIRST_TX_METHOD = "ftm";
	public static final String FIRST_TX_ID = "ftid";
	public static final String FIRST_TX_PORT = "ftp";
	public static final String FIRST_TX_SECURE = "fts";
	public static final String CONTACT_HEADER = "ch";
	public static final String LOCAL_TAG = "ltag";
	public static final String REMOTE_TAG = "rtag";
	public static final String VERSION = "v";
	public static final String REMOTE_CSEQ = "rc";
	public static final String LOCAL_CSEQ = "lc";
	public static final String DIALOG_METHOD = "dm";
	public static final String ENABLE_CSEQ_VALIDATION = "dc";
	public static final String DIALOG_STATE = "ds";

	public boolean b2buaChanged;
	public boolean eventChanged;
	public boolean remoteTargetChanged;
	public boolean terminateOnByeChanged;
	public boolean isReinviteChanged;
	public boolean storeFirstTxChanged;
	public boolean dialogStateChanged;
	public boolean isLatestTxServer;

	static AddressFactory addressFactory = null;
	static HeaderFactory headerFactory = null;
	boolean isCreated = false;
	private AtomicLong version = new AtomicLong(0);
	private String lastResponseStringified = null;

	static {
		try {
			headerFactory = SipFactory.getInstance().createHeaderFactory();
			addressFactory = SipFactory.getInstance().createAddressFactory();
		} catch (PeerUnavailableException e) {}
	}

	public AbstractHASipDialog(SIPTransaction transaction) {
		super(transaction);
		isCreated = true;
	}

	public AbstractHASipDialog(SIPClientTransaction transaction, SIPResponse sipResponse) {
		super(transaction, sipResponse);
		isCreated = true;
	}

    public AbstractHASipDialog(SipProviderImpl sipProvider, SIPResponse sipResponse) {
		super(sipProvider, sipResponse);
		isCreated = true;
	}

	/**
	 * Updates the local dialog transient attributes that were not serialized during the replication
	 * @param sipStackImpl the sip Stack Impl that reloaded this dialog from the distributed cache
	 */
	public void initAfterLoad(ClusteredSipStack sipStackImpl) {
		String transport = getLastResponseTopMostVia().getTransport();
		Iterator<SipProviderImpl> providers = sipStackImpl.getSipProviders();
		boolean providerNotFound = true;
		while(providers.hasNext()) {
			SipProviderImpl providerImpl = providers.next();
			if(providerImpl.getListeningPoint(transport) != null) {
				setSipProvider(providerImpl);
				providerNotFound = false;
			}
		}
		if(providerNotFound) {
			throw new RuntimeException("No providers found for transport="
					+ transport + " on this node. Make sure connectors are configured for this transport");
		}
		setStack((SIPTransactionStack)sipStackImpl);
		setAssigned();
		firstTransactionPort = getSipProvider().getListeningPoint(getLastResponseTopMostVia().getTransport()).getPort();
		ackProcessed = true;
//		ackSeen = true;
	}

	@SuppressWarnings("unchecked")
	public Map<String,Object> getMetaDataToReplicate() {
		Map<String,Object> dialogMetaData = new HashMap<String,Object>();
		dialogMetaData.put(VERSION, Long.valueOf(version.incrementAndGet()));
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : version " + version);
		}
		if(dialogStateChanged) {
			dialogMetaData.put(DIALOG_STATE, Integer.valueOf(getState().getValue()));
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : dialogState " + getState());
			}
			dialogStateChanged = false;
		}
		boolean firstTimeReplication = version.get() == 1;
		if(firstTimeReplication) {
			dialogMetaData.put(DIALOG_METHOD, getMethod());
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : dialog method " + getMethod());
			}
		}
		dialogMetaData.put(LAST_RESPONSE, getLastResponseStringified());
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : lastResponse " + getLastResponseStringified());
		}
		if(isReinviteChanged) {
			dialogMetaData.put(IS_REINVITE, isReInvite());
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isReInvite " + isReInvite());
			}
			isReinviteChanged = false;
		}
		final List<SIPHeader> routeList = new ArrayList<SIPHeader>();
		final Iterator<SIPHeader> it = getRouteSet();
		while (it.hasNext()) {
			SIPHeader sipHeader = (SIPHeader) it.next();
			routeList.add(sipHeader);
		}
		final String[] routes = new String[routeList.size()];
		int i = 0;
		for (SIPHeader sipHeader : routeList) {
			routes[i++] = sipHeader.getHeaderValue().toString();
		}
		dialogMetaData.put(ROUTE_LIST, routes);
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : routes " + routes);
		}
		if(terminateOnByeChanged) {
			dialogMetaData.put(TERMINATE_ON_BYE, Boolean.valueOf(isTerminatedOnBye()));
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : terminateOnBye " + isTerminatedOnBye());
			}
			terminateOnByeChanged = false;
		}
		if(remoteTargetChanged) {
			if(getRemoteTarget() != null) {
				dialogMetaData.put(REMOTE_TARGET, getRemoteTarget().toString());
			} else {
				dialogMetaData.put(REMOTE_TARGET, null);
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : remoteTarget " + getRemoteTarget());
			}
			remoteTargetChanged = false;
		}
		if(eventChanged) {
			if(getEventHeader() != null) {
				dialogMetaData.put(EVENT_HEADER, getEventHeader().toString());
			} else {
				dialogMetaData.put(EVENT_HEADER, null);
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : evenHeader " + getEventHeader());
			}
			eventChanged = false;
		}
		if(b2buaChanged) {
			dialogMetaData.put(B2BUA, Boolean.valueOf(isBackToBackUserAgent()));
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isB2BUA " + isBackToBackUserAgent());
			}
			b2buaChanged = false;
		}
		if(storeFirstTxChanged) {
			dialogMetaData.put(IS_SERVER, Boolean.valueOf(isServer()));
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isServer " + isServer());
			}
			dialogMetaData.put(FIRST_TX_SECURE, Boolean.valueOf(firstTransactionSecure));
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTxSecure " + firstTransactionSecure);
			}
			dialogMetaData.put(FIRST_TX_ID, firstTransactionId);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTransactionId " + firstTransactionId);
			}
			dialogMetaData.put(ENABLE_CSEQ_VALIDATION, isSequenceNumberValidation());
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : CSeq validation is " + isSequenceNumberValidation());
			}
			dialogMetaData.put(FIRST_TX_METHOD, firstTransactionMethod);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTransactionMethod " + firstTransactionMethod);
			}
			if(contactHeader != null) {
				// http://code.google.com/p/jain-sip/issues/detail?id=38
				dialogMetaData.put(CONTACT_HEADER, contactHeader.getHeaderValue());
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : contactHeader " + contactHeader);
			}
			storeFirstTxChanged = false;
		}
		dialogMetaData.put(IS_LATEST_TX_SERVER, Boolean.valueOf(isLatestTxServer));
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : isLatestTxServer " + isLatestTxServer);
		}
		dialogMetaData.put(REMOTE_TAG, getRemoteTag());
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : remoteTag " + getRemoteTag());
		}
		dialogMetaData.put(LOCAL_TAG, getLocalTag());
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : localTag " + getLocalTag());
		}
		dialogMetaData.put(REMOTE_CSEQ, Long.valueOf(getRemoteSeqNumber()));
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : remoteCSeq " + getRemoteSeqNumber());
		}
		dialogMetaData.put(LOCAL_CSEQ, Long.valueOf(getLocalSeqNumber()));
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : localCSeq " + getLocalSeqNumber());
		}

		return dialogMetaData;
	}

	public Object getApplicationDataToReplicate() {
		return getApplicationData();
	}

	public void setMetaDataToReplicate(Map<String, Object> metaData, boolean recreation) {
		final ReplicationStrategy replicationStrategy = ((ClusteredSipStack)getStack()).getReplicationStrategy();
		if(replicationStrategy == ReplicationStrategy.EarlyDialog) {
			Integer dialogState = (Integer) metaData.get(DIALOG_STATE);
			if(dialogState!= null) {
				// the call to super is very important otherwise it triggers replication on dialog recreation
				super.setState(dialogState);
			}
		} else {
			// the call to super is very important otherwise it triggers replication on dialog recreation
			super.setState(DialogState._CONFIRMED);
		}
		lastResponseStringified = (String) metaData.get(LAST_RESPONSE);
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : lastResponse " + lastResponseStringified);
		}
		String dialogMethod = (String) metaData.get(DIALOG_METHOD);
		if(dialogMethod!= null) {
			method = dialogMethod;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : dialog method " + method);
			}
		}
		version = new AtomicLong((Long)metaData.get(VERSION));
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : version " + version);
		}
		final Boolean isB2BUA = (Boolean) metaData.get(B2BUA);
		if(isB2BUA != null && isB2BUA == Boolean.TRUE) {
			setBackToBackUserAgent();
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isB2BUA " + isB2BUA);
			}
		}
		final Boolean isReinvite = (Boolean) metaData.get(IS_REINVITE);
		if(isReinvite != null) {
			super.setReInviteFlag(isReinvite);
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isReInvite " + isReinvite);
			}
		}
		final String eventHeaderStringified = (String) metaData.get(EVENT_HEADER);
		if(eventHeaderStringified != null) {
			try {
				super.setEventHeader((EventHeader)new EventParser(eventHeaderStringified).parse());
			} catch (ParseException e) {
				logger.logError("Unexpected exception while parsing a deserialized eventHeader", e);
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : evenHeader " + eventHeaderStringified);
			}
		}
		final String remoteTargetCache = (String) metaData.get(REMOTE_TARGET);
		if(remoteTargetCache != null) {
			Contact contact = new Contact();
			try {
				super.remotePartyStringified = remoteTargetCache;
				contact.setAddress(addressFactory.createAddress(remoteTargetCache));
				super.setRemoteTarget(contact);
			} catch (ParseException e) {
				logger.logError("Unexpected exception while parsing a deserialized remoteTarget address", e);
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : remoteTarget " + remoteTargetStringified);
			}
		}
		final Boolean terminateOnBye = (Boolean) metaData.get(TERMINATE_ON_BYE);
		if(terminateOnBye != null) {
			try {
				terminateOnBye(terminateOnBye);
			} catch (SipException e) {
				// exception is never thrown
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : terminateOnBye " + terminateOnBye);
			}
		}
		final String[] routes = (String[]) metaData.get(ROUTE_LIST);
		if(routes != null) {
			final RouteList routeList = new RouteList();
			for (String route : routes) {
				try {
					routeList.add((Route)headerFactory.createRouteHeader(addressFactory.createAddress(route)));
				} catch (ParseException e) {
					logger.logError("Unexpected exception while parsing a deserialized route address", e);
				}
			}
			setRouteList(routeList);
		}
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : routes " + routes);
		}
		final Boolean isServer = (Boolean) metaData.get(IS_SERVER);
		if(isServer != null) {
			firstTransactionSeen = true;
			firstTransactionIsServerTransaction = isServer.booleanValue();
			super.setServerTransactionFlag(isServer.booleanValue());
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : isServer " + isServer.booleanValue());
			}
		}
		final Boolean firstTxSecure = (Boolean) metaData.get(FIRST_TX_SECURE);
		if(firstTxSecure != null) {
			firstTransactionSecure = firstTxSecure;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTxSecure " + firstTxSecure);
			}
		}
		final String firstTxId = (String) metaData.get(FIRST_TX_ID);
		if(firstTxId != null) {
			firstTransactionId = firstTxId;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTransactionId " + firstTransactionId);
			}
		}
		final String firstTxMethod = (String) metaData.get(FIRST_TX_METHOD);
		if(firstTxMethod != null) {
			firstTransactionMethod = firstTxMethod;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : firstTransactionMethod " + firstTransactionMethod);
			}
		}
		if(recreation) {
			isLatestTxServer = (Boolean) metaData.get(IS_LATEST_TX_SERVER);
			if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("HA SIP Dialog is Server ? " + isServer() + ", isLatestTxServer ? " + isLatestTxServer);
			}
			// http://code.google.com/p/restcomm/issues/detail?id=2942
			// 	From and To Uris switch places in certain conditions
			if(isLatestTxServer) {
				if(logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
					logger.logDebug("dialog : switching parties on recreation " + getDialogIdToReplicate() + " localParty = " + getLocalParty());
				}
				Address remoteParty = getLocalParty();
				Address localParty = getRemoteParty();
				setLocalPartyInternal(localParty);
				setRemotePartyInternal(remoteParty);
				long remoteCSeq = getLocalSeqNumber();
				long localCSeq = getRemoteSeqNumber();
				localSequenceNumber = localCSeq;
				remoteSequenceNumber = remoteCSeq;
			}
		}

		String remoteTag = (String) metaData.get(REMOTE_TAG);
		setRemoteTagInternal(remoteTag);
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : remoteTag " + getRemoteTag());
		}
		String localTag = (String) metaData.get(LOCAL_TAG);
		setLocalTagInternal(localTag);
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug(getDialogIdToReplicate() + " : localTag " + getLocalTag());
		}
		Long remoteCSeq = (Long) metaData.get(REMOTE_CSEQ);
		if(remoteCSeq != null) {
			long cseq = remoteCSeq.longValue();
			if(getRemoteSeqNumber()>cseq) {
				if (logger.isLoggingEnabled(StackLogger.TRACE_INFO)) {
					logger.logInfo("Concurrency problem. Nodes are out" +
							" of sync. We will assume the local CSeq is the valid one. Enable request affinity to avoid this problem, remoteSequenceNumber=" +
							getRemoteSeqNumber() + " while other node's remote CSeq" +
									" number=" + cseq);
				}
				// No need to update the number, it is greater, http://code.google.com/p/restcomm/issues/detail?id=2051
			} else {
				setRemoteSequenceNumber(cseq);
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : remoteCSeq " + getRemoteSeqNumber());
			}
		}
		Long localCSeq = (Long) metaData.get(LOCAL_CSEQ);
		if(localCSeq != null) {
			long cseq = localCSeq.longValue();
			if(localSequenceNumber>cseq) {
				if (logger.isLoggingEnabled(StackLogger.TRACE_INFO)) {
					logger.logInfo("Concurrency problem. Nodes are out" +
							" of sync. We will assume the local CSeq is the valid one. Enable request affinity to avoid this problem, localSequenceNumber="
							+ localSequenceNumber+ " while other node's local CSeq" +
									" number=" + cseq);
				}
				// No need to update the number, it is greater, http://code.google.com/p/restcomm/issues/detail?id=2051
			} else {
				localSequenceNumber = cseq;
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : localCSeq " + getLocalSeqNumber());
			}
		}
		final Boolean enableCSeqValidation = (Boolean) metaData.get(ENABLE_CSEQ_VALIDATION);
		if(enableCSeqValidation != null) {
			if(!enableCSeqValidation) disableSequenceNumberValidation();
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(getDialogIdToReplicate() + " : CSeq validation is " + enableCSeqValidation);
			}
		}
	}

	public void setApplicationDataToReplicate(Object appData) {
		// the call to super is very important otherwise it triggers replication on dialog recreation
		super.setApplicationData(appData);
	}

	// not needed anymore the setLastResponse on final response will take care of it automatically
//	@Override
//	public void setState(int state) {
//		DialogState oldState = getState();
//		long previousVersion = -1;
//		if(version != null) {
//			previousVersion= version.get();
//		}
//		super.setState(state);
//		DialogState newState = getState();
//		// we replicate only if the state has really changed
//		// the fact of setting the last response upon recreation will trigger setState to be called and so the replication
//		// so we make sure to replicate only if the dialog has been created
//		// also  we replicate only if the version is the same, otherwise it means setState already triggered a replication by putting the dialog in the stack and therefore in the cache
//		if(!newState.equals(oldState) & newState == DialogState.CONFIRMED && previousVersion != -1 && previousVersion == version.get()){
//			replicateState();
//		}
//	}

	protected abstract void replicateState();

	public void setLocalTagInternal(String localTag) {
		super.myTag = localTag;
	}

	public void setRemoteTagInternal(String remoteTag) {
		super.hisTag = remoteTag;
	}

	public void setLocalPartyInternal(Address localParty) {
		super.localParty = localParty;
	}

	public void setRemotePartyInternal(Address remoteParty) {
		super.remoteParty = remoteParty;
	}

	public void setContactHeader(ContactHeader contactHeader) {
		this.contactHeader = (Contact) contactHeader;
	}

	@Override
	public void setLastResponse(SIPTransaction transaction,
			SIPResponse sipResponse) {
		// version can be null on dialog recreation
		if(version != null) {
			boolean lastResponseChanged = false;
			long previousVersion = version.get();
			// for 2xx w/o 1xx
			final ReplicationStrategy replicationStrategy = ((ClusteredSipStack)getStack()).getReplicationStrategy();
			// set to confirmed dialog strategy at least
			int lowerStatusCodeToReplicateOn = 200;
			if(replicationStrategy == ReplicationStrategy.EarlyDialog) {
				lowerStatusCodeToReplicateOn = 101;
			}
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(dialogId  + " lowerStatusCodeToReplicateOn = " + lowerStatusCodeToReplicateOn);
				logger.logDebug(dialogId  + " lastResponseStr = " + lastResponseStringified);
				logger.logDebug(dialogId  + " sipResponse = " + sipResponse);
			}
			if(sipResponse != null && getLastResponseStringified() == null && sipResponse.getStatusCode() >= lowerStatusCodeToReplicateOn) {
				lastResponseChanged = true;
			}
			String responseStringified = sipResponse.toString();
			if(sipResponse != null && getLastResponseStringified() != null && sipResponse.getStatusCode() >= lowerStatusCodeToReplicateOn && !responseStringified.equals(this.getLastResponseStringified())) {
				lastResponseChanged = true;
			}
			super.setLastResponse(transaction, sipResponse);
			lastResponseStringified = responseStringified;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug(dialogId  + " lastResponseChanged = " + lastResponseChanged);
				logger.logDebug(dialogId  + " previousVersion = " + previousVersion);
				logger.logDebug(dialogId  + " currentVersion = " + version.get());
			}
			// we replicate only if the version is the same, otherwise it means lastResponse already triggered a replication by putting the dialog in the stack and therefore in the cache
			if(lastResponseChanged && previousVersion == version.get()) {
				// don't consider it a retrans even if it is on the new node taking over
				if(replicationStrategy == ReplicationStrategy.EarlyDialog) {
					sipResponse.setRetransmission(false);
				}
				replicateState();
			}
		}
		// causes REINVITE after NOTIFY on failover to fail with NPE since method attribute is not yet initialized
//		else {
//			super.setLastResponse(transaction, sipResponse);
//		}
	}

	public void setLastResponse(SIPResponse lastResponse) {
		// the call to super is very important otherwise it triggers replication on dialog recreation
		super.setLastResponse(null, lastResponse);
	}

	/**
	 * @return the isRemoteTagSet
	 */
	public boolean isRemoteTagSet() {
		return hisTag != null && hisTag.trim().length() > 0 ? true : false;
	}

	/**
	 * @return the isRemoteTagSet
	 */
	public boolean isLocalTagSet() {
		return myTag != null && myTag.trim().length() > 0 ? true : false;
	}

	/**
	 * @return the version
	 */
	public long getVersion() {
		return version.get();
	}

	public String getDialogIdToReplicate() {
		return getDialogId();
		// No need for this anymore since we replicate only when the last response is a final one
//		String cid = this.getCallId().getCallId();
//		StringBuilder retval = new StringBuilder(cid);
//        retval.append(Separators.COLON);
//        retval.append(myTag);
//        retval.append(Separators.COLON);
//        retval.append(hisTag);
//        return retval.toString().toLowerCase();
	}

	public String getLastResponseStringified() {
		return lastResponseStringified;
	}

	@Override
	public void setReInviteFlag(boolean reInviteFlag) {
		boolean oldReinvite = isReInvite();
		super.setReInviteFlag(reInviteFlag);
		if(reInviteFlag != oldReinvite) {
			isReinviteChanged = true;
		}
	}

	@Override
	protected void storeFirstTransactionInfo(SIPDialog dialog, SIPTransaction transaction) {
		super.storeFirstTransactionInfo(dialog, transaction);
		storeFirstTxChanged = true;
	}

	// http://code.google.com/p/restcomm/issues/detail?id=2942
	// 	From and To Uris switch places in certain conditions
	@Override
	public boolean addTransaction(SIPTransaction transaction) {
		if(transaction instanceof ServerTransaction) {
			 isLatestTxServer = true;
		} else {
			 isLatestTxServer = false;
		}
		return super.addTransaction(transaction);
	}

	@Override
	public void setRemoteTarget(ContactHeader contact) {
		super.setRemoteTarget(contact);
		remoteTargetChanged = true;
	}

	@Override
	public void setState(int state) {
		DialogState oldState = this.getState();
		super.setState(state);
		final ReplicationStrategy replicationStrategy = ((ClusteredSipStack)getStack()).getReplicationStrategy();
		if(replicationStrategy == ReplicationStrategy.EarlyDialog && (oldState == null  || oldState.getValue() != state
			// commented as part of https://github.com/Mobicents/jain-sip.ha/pull/1
			/*&& state != DialogState.TERMINATED.getValue()*/)) {
			dialogStateChanged = true;
			if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
				logger.logDebug("dialogStateChanged");
			}
		}
	}
}


/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package gov.nist.javax.sip.stack;

import gov.nist.core.CommonLogger;
import gov.nist.core.StackLogger;
import gov.nist.javax.sip.SipProviderImpl;
import gov.nist.javax.sip.message.SIPResponse;

import javax.sip.DialogState;

import org.mobicents.ha.javax.sip.ClusteredSipStack;
import org.mobicents.ha.javax.sip.ReplicationStrategy;
import org.mobicents.ha.javax.sip.cache.SipCacheException;

/**
 * Extends the standard NIST SIP Stack Dialog so that it gets replicated when the dialog state is confirmed
 *
 * @author jean.deruelle@gmail.com
 *
 */
public class ConfirmedNoAppDataReplicationSipDialog extends AbstractHASipDialog {

	private static StackLogger logger = CommonLogger.getLogger(ConfirmedNoAppDataReplicationSipDialog.class);

	private static final long serialVersionUID = -779892668482217624L;

	public ConfirmedNoAppDataReplicationSipDialog(SIPTransaction transaction) {
		super(transaction);
	}

	public ConfirmedNoAppDataReplicationSipDialog(SIPClientTransaction transaction, SIPResponse sipResponse) {
		super(transaction, sipResponse);
	}

    public ConfirmedNoAppDataReplicationSipDialog(SipProviderImpl sipProvider, SIPResponse sipResponse) {
		super(sipProvider, sipResponse);
	}

	/*
	 *
	 */
	protected void replicateState() {
		final DialogState dialogState = getState();
		final ReplicationStrategy replicationStrategy = ((ClusteredSipStack)getStack()).getReplicationStrategy();
		boolean replicationStateVsDialogStateOK= false;
		if (logger.isLoggingEnabled(StackLogger.TRACE_DEBUG)) {
			logger.logDebug("dialogState = " + dialogState + ", replicationStrategy = " + replicationStrategy);
		}
		if(dialogState == DialogState.CONFIRMED && (replicationStrategy == ReplicationStrategy.ConfirmedDialog|| replicationStrategy == ReplicationStrategy.ConfirmedDialogNoApplicationData)) {
			replicationStateVsDialogStateOK = true;
		}
		if((dialogState == DialogState.EARLY || dialogState == DialogState.CONFIRMED ||
			// Added as part of https://github.com/Mobicents/jain-sip.ha/pull/1
			dialogState == DialogState.TERMINATED)
			&& replicationStrategy == ReplicationStrategy.EarlyDialog) {
			replicationStateVsDialogStateOK = true;
		}
		if (replicationStateVsDialogStateOK && isCreated && super.dialogId != null && isRemoteTagSet() && isLocalTagSet() && getStack().getDialog(getDialogIdToReplicate()) != null) {
			try {
				((ClusteredSipStack)getStack()).getSipCache().putDialog(this);
			} catch (SipCacheException e) {
				logger.logError("problem storing dialog " + getDialogId() + " into the distributed cache", e);
			}
		}
	}

	public Object getApplicationDataToReplicate() {
		return null;
	}
}


/*
 * 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.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
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.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 per connector so it can support multiple network interfaces and transports at the same time for outbound traffic
 *  See https://github.com/RestComm/jain-sip.ha/issues/9
 *

 *
 * @author Jean Deruelle
 *
 */
public class MultiNetworkLoadBalancerHeartBeatingServiceImpl implements LoadBalancerHeartBeatingService, MultiNetworkLoadBalancerHeartBeatingServiceImplMBean {

	private static StackLogger logger = CommonLogger.getLogger(MultiNetworkLoadBalancerHeartBeatingServiceImpl.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 jvmRoute for this node
	protected String jvmRoute;
    //the balancers names to send heartbeat to and our health info
	// 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
	protected Map<SipLoadBalancer, ConcurrentHashMap<String, SIPNode>> register = new ConcurrentHashMap<SipLoadBalancer, ConcurrentHashMap<String, SIPNode>>();
	protected Map<SipLoadBalancer, Set<ListeningPoint>> connectors = new ConcurrentHashMap<SipLoadBalancer, Set<ListeningPoint>>();
	//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 Set<LoadBalancerHeartBeatingListener> loadBalancerHeartBeatingListeners;
	// https://github.com/RestComm/jain-sip.ha/issues/3
	protected boolean useLoadBalancerForAllConnectors;

	ObjectName oname = null;

    public MultiNetworkLoadBalancerHeartBeatingServiceImpl() {
		loadBalancerHeartBeatingListeners = new CopyOnWriteArraySet<LoadBalancerHeartBeatingListener>();
		useLoadBalancerForAllConnectors = true;
	}

	public void init(ClusteredSipStack clusteredSipStack,
			Properties stackProperties) {
		sipStack = clusteredSipStack;
        sipStackProperties = stackProperties;
		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) {
			started = true;
		}
//    	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) {
		throw new UnsupportedOperationException("This algorithm only allows to add a Load Balancer tied to a sip connector");
	}

	/**
     * {@inheritDoc}
     */
	public boolean addBalancer(String hostName, int sipPort, int httpPort, int index, int rmiPort) {
		throw new UnsupportedOperationException("This algorithm only allows to add a Load Balancer tied to a sip connector");
	}

	/**
     * {@inheritDoc}
     */
	public boolean removeBalancer(String addr, int sipPort, int httpPort, int rmiPort) {
		throw new UnsupportedOperationException("This algorithm only allows to remove a Load Balancer tied to a sip connector");
	}

	/**
     * {@inheritDoc}
     */
	public boolean removeBalancer(String hostName, int sipPort, int httpPort, int index, int rmiPort) {
		throw new UnsupportedOperationException("This algorithm only allows to remove a Load Balancer tied to a sip connector");
	}

	protected void updateConnectorsAsSIPNode(SipLoadBalancer loadBalancer) {
		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
		Set<ListeningPoint> listeningPoints = connectors.get(loadBalancer);
		if(listeningPoints == null) {
			if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)) {
				logger.logInfo("No listening points defined for " + loadBalancer);
			}
			return;
		}
		Iterator<ListeningPoint> listeningPointIterator = listeningPoints.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);
						ConcurrentHashMap<String, SIPNode> sipNodes = register.get(loadBalancer);
						if(sipNodes == null) {
							sipNodes = new ConcurrentHashMap<String, SIPNode>();
							sipNodes.put(ipAddress, node);
							if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
								logger.logTrace("Added a sip Node with the key [" + ipAddress + "]");
							}
							register.put(loadBalancer, sipNodes);
						} else {
							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);
						// https://github.com/RestComm/sip-servlets/issues/172
						if(loadBalancer.getCustomInfo() != null && !loadBalancer.getCustomInfo().isEmpty()) {
							for(Entry<Object, Object> entry : loadBalancer.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());
							}
						}

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

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

						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() {

		Thread.currentThread().setContextClassLoader(NodeRegisterRMIStub.class.getClassLoader());
		for(SipLoadBalancer loadBalancerToPing: register.keySet()) {
			try {
				ConcurrentHashMap<String, SIPNode> sipNodes = register.get(loadBalancerToPing);
				if(sipNodes.isEmpty()) {
		    		logger.logInfo("Computing SIP Nodes to be sent to the LB as the list is currently empty");
		    		updateConnectorsAsSIPNode(loadBalancerToPing);
				}
				ArrayList<SIPNode> info = new ArrayList<SIPNode>(sipNodes.values());
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
		            logger.logTrace("Pinging balancers with info[" + info + "]");
		        }

				long startTime = System.currentTimeMillis();
				Registry registry = LocateRegistry.getRegistry(loadBalancerToPing.getAddress().getHostAddress(), loadBalancerToPing.getRmiPort());
				NodeRegisterRMIStub reg=(NodeRegisterRMIStub) registry.lookup("SIPBalancer");
                if(reachableCheck) {
                    ArrayList<SIPNode> reachableInfo = getReachableSIPNodeInfo(loadBalancerToPing.getAddress(), info);
                    info = reachableInfo;
                    if(reachableInfo.isEmpty()) {
                        logger.logWarning("All connectors are unreachable from the balancer");
                    }
                }
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
				    logger.logTrace("Pinging " + loadBalancerToPing + " with the following Node Info [" + info + "]");
				}
				// https://github.com/RestComm/jain-sip.ha/issues/14
				// notify the listeners
				for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
					loadBalancerHeartBeatingListener.pingingloadBalancer(loadBalancerToPing);
				}
				reg.handlePing(info);
				// notify the listeners
				for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
					loadBalancerHeartBeatingListener.pingedloadBalancer(loadBalancerToPing);
				}
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
				    logger.logTrace("Pinged " + loadBalancerToPing + "  with the following Node Info [" + info + "]");
				}
				loadBalancerToPing.setDisplayWarning(true);
				if(!loadBalancerToPing.isAvailable()) {
					logger.logInfo("Keepalive: SIP Load Balancer Found! " + loadBalancerToPing);
				}
				loadBalancerToPing.setAvailable(true);
				startTime = System.currentTimeMillis() - startTime;
				if(startTime>200)
					logger.logWarning("Heartbeat sent too slow in " + startTime + " millis at " + System.currentTimeMillis());

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

	/**
	 * 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(InetAddress balancerAddr, ArrayList<SIPNode> info) {
		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) {
					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() {
		Thread.currentThread().setContextClassLoader(NodeRegisterRMIStub.class.getClassLoader());
		for(SipLoadBalancer loadBalancerToRemove: register.keySet()) {
			try {
				Registry registry = LocateRegistry.getRegistry(loadBalancerToRemove.getAddress().getHostAddress(),loadBalancerToRemove.getRmiPort());
				NodeRegisterRMIStub reg=(NodeRegisterRMIStub) registry.lookup("SIPBalancer");
				ArrayList<SIPNode> info = new ArrayList<SIPNode>(register.get(loadBalancerToRemove).values());
				reg.forceRemoval(info);
				if(!loadBalancerToRemove.isAvailable()) {
					logger.logInfo("Remove: SIP Load Balancer Found! " + loadBalancerToRemove);
					loadBalancerToRemove.setDisplayWarning(true);
				}
				loadBalancerToRemove.setAvailable(true);
				if(logger.isLoggingEnabled(StackLogger.TRACE_TRACE)) {
					logger.logTrace("Finished removed, Gathered info[" + info + "]");
				}
			} catch (IOException e) {
				if(loadBalancerToRemove.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.");
					loadBalancerToRemove.setDisplayWarning(false);
				}
				loadBalancerToRemove.setAvailable(true);
			} catch (Exception e) {
				if(loadBalancerToRemove.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);
					loadBalancerToRemove.setDisplayWarning(false);
				}
				loadBalancerToRemove.setAvailable(true);
			}
		}
	}

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

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

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

	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) {
		throw new UnsupportedOperationException("This algorithm doesn't let you specify a load balancer without connector, please use instead " + LoadBalancerHeartBeatingServiceImpl.class.getName());
	}

	public void removeSipConnector(ListeningPoint listeningPoint) {
		throw new UnsupportedOperationException("This algorithm doesn't let you specify a load balancer without connector, please use instead " + LoadBalancerHeartBeatingServiceImpl.class.getName());
	}

	public void addSipConnector(ListeningPoint listeningPoint, SipLoadBalancer loadBalancer) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)){
			logger.logInfo("Adding Listening Point to be using the Load Balancer " + loadBalancer + " 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;
		Set<ListeningPoint> sipConnectors = connectors.get(loadBalancer);
		if(sipConnectors == null) {
			sipConnectors = new CopyOnWriteArraySet<ListeningPoint>();
			connectors.put(loadBalancer, sipConnectors);
		}
		sipConnectors.add(listeningPoint);
		updateConnectorsAsSIPNode(loadBalancer);
		// notify the listeners
		for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
			loadBalancerHeartBeatingListener.loadBalancerAdded(loadBalancer);
		}
	}

	public void removeSipConnector(ListeningPoint listeningPoint, SipLoadBalancer loadBalancer) {
		if(logger.isLoggingEnabled(StackLogger.TRACE_INFO)){
			logger.logInfo("Removing Listening Point to be using the Load Balancer " + loadBalancer + " 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;
		Set<ListeningPoint> sipConnectors = connectors.get(loadBalancer);
		sipConnectors.remove(listeningPoint);
		updateConnectorsAsSIPNode(loadBalancer);
		// notify the listeners
		for (LoadBalancerHeartBeatingListener loadBalancerHeartBeatingListener : loadBalancerHeartBeatingListeners) {
			loadBalancerHeartBeatingListener.loadBalancerRemoved(loadBalancer);
		}
	}
}