/*
 * ident	"@(#)Transact.java	1.2	99/05/10 SMI"
 *
 * Copyright (c) 1999 by Sun Microsystems, Inc.
 * All rights reserved.
 *
 */

//  SCCS Status:      @(#)Transact.java	1.11	09/03/98
//  Transact.java:    Low level details of performing an SLP
//                    network transaction.
//  Author:           James Kempf
//  Created On:       Tue May 12 13:09:40 1998
//  Last Modified By: James Kempf
//  Last Modified On: Thu Mar 11 15:01:52 1999
//  Update Count:     169
//

package com.sun.slp;

import java.util.*;
import java.net.*;
import java.io.*;

/**
 * Transact performs the low level details for transacting an SLP network
 * query. Based on the v1 Transact class by Erik Guttman.
 * Note that, in the future, this class may spin separate threads
 * for DA requests as well.
 *
 * @version 1.11 98/09/03
 * @author James Kempf
 */

class Transact extends Object implements Runnable {

    // Cache of open TCP sockets.

    private static final Hashtable TCPSocketCache = new Hashtable();

    // SLP config object.

    protected static SLPConfig config = null;

    // Message to send.

    protected SrvLocMsg msgOut = null;

    // Vector of return values.

    protected Vector returns = null;

    // Timeout for multicast convergence. Varies if it's DA discovery or
    //  request multicast.

    protected int[] MSTimeouts;

    // Maximum results desired for multicast.

    protected int maxResults = 0;

    // Exception to throw.

    protected ServiceLocationException exErr = null;

    // Multicast address to use.

    protected InetAddress address = null;

    // If this is true, continue multicast after the first set of stuff
    //  is found. Exit when three tries have happened without finding
    //  anything.

    boolean continueAfterFound = false;

    /**
     * Perform a query to the SLP network. The multicast query is performed
     * in a separate thread for performance reasons. DAs having the
     * same scope set are queried until one answers. These DAs form
     * an equivalence class.
     *
     * @param daEquivClasses Vector of DATable.DARecord objects in the
     *			  same equivalence clase w.r.t. scopes.
     * @param uniMsg A unicast message to send.
     * @param multiMsg A multicast message to send.
     * @param address Multicast address to use.
     * @return Vector of SrvLocMsg objects with results.
     */

    static Vector
	transactUA(Vector daEquivClasses,
		   SrvLocMsg uniMsg,
		   SrvLocMsg multiMsg,
		   InetAddress address)
	throws ServiceLocationException {

	// If we need to multicast, then start the multicast thread.

	Vector ret = new Vector();
	Thread multiThread = null;
	Transact tracon = null;

	if (multiMsg != null) {

	    // Create a new Transact multicast thread.

	    tracon =
		new Transact(multiMsg,
			     ret,
			     config.getMulticastTimeouts(),
			     config.getMaximumResults(),
			     address,
			     false);

	    multiThread = new Thread(tracon);

	    // Run it.

	    multiThread.start();

	}

	// Go through the msgTable doing all the DAs.

	ServiceLocationException exx = null;

	if (daEquivClasses != null) {
	    exx =
		transactUnicastMsg(daEquivClasses,
				   uniMsg,
				   ret,
				   config.getMaximumResults());

	}

	// Wait until the TransactConverge thread is done, if necessary.

	if (multiThread != null) {

	    try {
		multiThread.join();

	    } catch (InterruptedException ex) {

	    }

	}

	// If there was a problem in either the multicast thread or in
	//  the unicast call, throw an exception, but *only* if no
	//  results came back.

	if (ret.size() <= 0) {

	    if (exx != null) {
		short err = exx.getErrorCode();

		if (err != ServiceLocationException.VERSION_NOT_SUPPORTED &&
		    err != ServiceLocationException.INTERNAL_ERROR &&
		    err != ServiceLocationException.OPTION_NOT_SUPPORTED &&
		    err != ServiceLocationException.REQUEST_NOT_SUPPORTED) {
		    throw exx;

		}

	    }

	    if (tracon != null && tracon.exErr != null) {
		short err = tracon.exErr.getErrorCode();

		if (err != ServiceLocationException.VERSION_NOT_SUPPORTED &&
		    err != ServiceLocationException.INTERNAL_ERROR &&
		    err != ServiceLocationException.OPTION_NOT_SUPPORTED &&
		    err != ServiceLocationException.REQUEST_NOT_SUPPORTED) {
		    throw tracon.exErr;

		}
	    }
	}


	// Return the result to the client.

	return ret;

    }

    /**
     * Transact a message with DAs. Put the returned SrvLocMsg
     * object into the Vector ret.
     *
     * @param daEquivClasses Vector of DATable.DARecord objects in the
     *			   same equivalence clase w.r.t. scopes.
     * @param msg SrvLocMsg Message to send.
     * @param ret Vector for returns.
     * @param maxResults Maximum results expected.
     * @return A ServiceLocationException object if an exception occured.
     * @exception ServiceLocationException
     *            If results cannot be obtained in the timeout interval
     *            specified in the 'config.' or
     *            If networking resources cannot be obtained or used
     *            effectively.
     */
    static ServiceLocationException
	transactUnicastMsg(Vector daEquivClasses,
			   SrvLocMsg msg,
			   Vector ret,
			   int maxResults) {

	// Get the config object if we need it.

	if (config == null) {
	    config = SLPConfig.getSLPConfig();

	}

	DatagramSocket ds = null;
	int i, n = daEquivClasses.size();
	ServiceLocationException exx = null;
	InetAddress addr = null;
	int numReplies = 0;
	DATable daTable = DATable.getDATable();

	try {

	    // Go through the DA address equivalence classes we need
	    //  to query.

	    for (i = 0; i < n && numReplies < maxResults; i++) {

		DATable.DARecord rec =
		    (DATable.DARecord)daEquivClasses.elementAt(i);
		Vector daAddresses = (Vector)rec.daAddresses.clone();

		// Get a new outgoing socket.

		if (ds == null) {
		    ds = new DatagramSocket();

		}

		// Go through the DA addresses until we get a reply from one.

		Enumeration en = daAddresses.elements();
		SrvLocHeader mhdr = msg.getHeader();

		while (en.hasMoreElements()) {

		    try {

			addr = (InetAddress)en.nextElement();

			if (config.traceDATraffic()) {
			    config.writeLog("sending_da_trace",
					    new Object[] {
				Integer.toHexString(mhdr.xid),
				    addr});

			}

			// Get the reply message if any.

			SrvLocMsg rply = transactDatagramMsg(ds, addr, msg);

			if (!filterRply(msg, rply, addr)) {
			    continue;

			}

			SrvLocHeader rhdr = rply.getHeader();

			if (config.traceDATraffic()) {
			    config.writeLog("reply_da_trace",
					    new Object[] {
				Integer.toHexString(rhdr.xid),
				    addr});

			}

			// If overflow, try TCP.

			if (rhdr.overflow) {
			    if (config.traceDATraffic()) {
				config.writeLog("tcp_send_da_trace",
						new Object[] {
				    Integer.toHexString(mhdr.xid),
					addr});

			    }

			    rply = transactTCPMsg(addr, msg, false);

			    if (config.traceDATraffic()) {
				config.writeLog("tcp_reply_da_trace",
						new Object[] {
				    (msg == null ? "<null>":
				     Integer.toHexString(mhdr.xid)),
					addr});

			    }

			    if (rply == null) {
				continue;

			    }

			}

			// Increment number of replies we received.

			SrvLocHeader hdr = rply.getHeader();

			numReplies += hdr.iNumReplies;

			// Add to return vector.

			ret.addElement(rply);

			// Break out of the loop, since we only need one in
			//  this equivalence class.

			break;

		    } catch (ServiceLocationException ex) {

			config.writeLog("da_exception_trace",
					new Object[] {
			    new Short(ex.getErrorCode()),
				addr,
				ex.getMessage()});

			// In case we are querying more than one DA, we
			// save th exception, returning it to the caller to
			// decide if it should be thrown. We ignore DA_BUSY,
			// though, since the DA may free up later.

			short errCode = ex.getErrorCode();

			if (errCode != ServiceLocationException.DA_BUSY) {
			    exx = ex;

			}

			// If the error code is NETWORK_TIMED_OUT, then remove
			//  this DA from the DA table. If it's just down
			//  temporarily, we'll get it next time we go to
			//  the server to get the DA addresses.

			if (errCode ==
			    ServiceLocationException.NETWORK_TIMED_OUT) {

			    if (config.traceDATraffic()) {
				config.writeLog("da_drop",
						new Object[] {
				    addr, rec.scopes});

			    }

			    daTable.removeDA(addr, rec.scopes);

			}
		    }
		}
	    }

	} catch (SocketException ex) {
	    exx =
		new ServiceLocationException(
				ServiceLocationException.NETWORK_ERROR,
				"socket_creation_failure",
				new Object[] {addr, ex.getMessage()});

	} finally {

	    // Clean up socket.

	    if (ds != null) {
		ds.close();
	    }
	}

	return exx;
    }

    /**
     * Transact a message via. UDP. Try a maximum of three times if
     * a timeout.
     *
     * @param ds The datagram socket to use.
     * @param addr The DA to contact.
     * @param msg The message to send.
     * @return The SrvLocMsg returned or null if none.
     * @exception ServiceLocationException Due to errors in parsing message.
     */

    static private SrvLocMsg
	transactDatagramMsg(DatagramSocket ds, InetAddress addr, SrvLocMsg msg)
	throws ServiceLocationException {

	SrvLocMsg rply = null;
	byte[] outbuf = getBytes(msg, false, false);
	byte[] inbuf = new byte[Defaults.iReadMaxMTU];

	// Construct the datagram packet to send.
	
	DatagramPacket dpReply =
	    new DatagramPacket(inbuf, inbuf.length);
	DatagramPacket dpRequest =
	    new DatagramPacket(outbuf, outbuf.length, addr, Defaults.iSLPPort);
	int[] timeouts = config.getDatagramTimeouts();

	// Resend for number of timeouts in timeout interval.

	int i;

	for (i = 0; i < timeouts.length; i++) {

	    // Catch timeout and IO errors.

	    try {

		ds.setSoTimeout(timeouts[i]);
		ds.send(dpRequest);
		ds.receive(dpReply);

		// Process result into a reply object.

		DataInputStream dis =
		    new DataInputStream(
			new ByteArrayInputStream(dpReply.getData()));

		rply = internalize(dis, addr);
		break;

	    } catch (InterruptedIOException ex) {

		// Did not get it on the first timeout, try again.

		if (config.traceDrop()|| config.traceDATraffic()) {
		    config.writeLog("udp_timeout",
				    new Object[] {addr});

		}

		continue;

	    } catch (IOException ex) {
		Object[] message = {addr, ex.getMessage()};

		if (config.traceDrop() || config.traceDATraffic()) {
		    config.writeLog("datagram_io_error",
				    message);

		}

		throw
		    new ServiceLocationException(
				ServiceLocationException.NETWORK_ERROR,
				"datagram_io_error",
				message);

	    }
	}

	// If nothing, then we've timed out. DAs with no matching
	//  info should at least return a reply.

	if (rply == null) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.NETWORK_TIMED_OUT,
				"udp_timeout",
				new Object[] {addr});

	}

	return rply;
    }

    /**
     * Transact a message using TCP, since the reply was too big.
     * @parameter addr Address of the DA to contact.
     * @parameter msg  The message object to use.
     * @parameter cacheIt Cache socket, if new.
     * @return  The SrvLocMsg returned if any.
     * @exception ServiceLocationException
     *            If results cannot be obtained in the timeout interval
     *            specified in the 'config.'
     *            If networking resources cannot be obtained or used
     *            effectively.
     */

    static SrvLocMsg
	transactTCPMsg(InetAddress addr, SrvLocMsg msg, boolean cacheIt)
	throws ServiceLocationException {

	// Get the config object if we need it.

	if (config == null) {
	    config = SLPConfig.getSLPConfig();

	}

	SrvLocMsg rply = null;

	try {

	    // Transact the message, taking care of socket caching.

	    rply = transactMsg(addr, msg, cacheIt, true);

	} catch (InterruptedIOException ex) {
	    Object[] message = {addr};

	    if (config.traceDrop()|| config.traceDATraffic()) {
		config.writeLog("tcp_timeout",
				message);

	    }

	    throw
		new ServiceLocationException(
				ServiceLocationException.NETWORK_TIMED_OUT,
				"tcp_timeout",
				message);

	} catch (IOException ex) {
	    Object[] message = {addr, ex.getMessage()};

	    if (config.traceDrop() || config.traceDATraffic()) {
		config.writeLog("tcp_io_error",
				message);

	    }

	    throw
		new ServiceLocationException(
				ServiceLocationException.NETWORK_ERROR,
				"tcp_io_error",
				message);

	}

	// Filter reply for nulls, invalid xid.

	if (!filterRply(msg, rply, addr)) {
	    return null;

	}

	return rply;
    }

    // Uncache a socket.

    static private void uncacheSocket(InetAddress addr, Socket s) {

	try {

	    s.close();

	} catch (IOException ex) {

	}

	TCPSocketCache.remove(addr);

    }

    // Get a (possibly cached) TCP socket, cache it if cache is on.

    static private Socket getTCPSocket(InetAddress addr, boolean cacheIt)
	throws IOException {

	Socket s = null;

	// We use the cached socket if we've got it.

	s = (Socket)TCPSocketCache.get(addr);

	if (s == null) {
	    s = new Socket(addr, Defaults.iSLPPort);

	    // Set it so the socket will block for fixed timeout.

	    s.setSoTimeout(config.getTCPTimeout());

	}

	// We cache it if we're supposed to.

	if (cacheIt) {
	    TCPSocketCache.put(addr, s);

	}

	return s;
    }

    // Transact the message, using cached socket if necessary. Retry if
    //  flag is true.

    static private SrvLocMsg
	transactMsg(InetAddress addr,
		    SrvLocMsg msg,
		    boolean cacheIt,
		    boolean retry)
	throws InterruptedIOException, IOException, ServiceLocationException {

	Socket s = null;
	byte outbuf[] = getBytes(msg, false, true);

	try {

	    s = getTCPSocket(addr, cacheIt);

	    DataOutputStream dos = new DataOutputStream(s.getOutputStream());
	    DataInputStream dis = new DataInputStream(s.getInputStream());

	    // In case the server cuts us off...

	    try {

		// Only one thread at a time gets to use this socket, in case
		//  it was cached. Otherwise, we *may* get interleaved i/o.

		synchronized (s) {

		    // Send the request.
	
		    dos.write(outbuf, 0, outbuf.length);

		    // Read reply.

		    return internalize(dis, addr);

		}

	    } catch (IOException ex) {

		// Uncache it, get a new one. If that one doesn't work, we're
		//  hosed.

		uncacheSocket(addr, s);
	
		s = null;

		if (!retry) {
		    throw ex;

		}

		// Recursively call ourselves to take care of this, but
		//  don't retry it.

		return transactMsg(addr, msg, cacheIt, false);

	    }

	} finally {

	    if (s != null && !cacheIt) {
		uncacheSocket(addr, s);

	    }
	}
    }

    // Externalize the message into bytes.

    static protected byte[] getBytes(SrvLocMsg slm,
				     boolean isMulti,
				     boolean isTCP)
	throws ServiceLocationException {

	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	SrvLocHeader hdr = slm.getHeader();

	hdr.externalize(baos, isMulti, isTCP);

	byte[] outbuf = baos.toByteArray();

	// Check if it excceds the output buffer length.

	if (hdr.overflow) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.BUFFER_OVERFLOW,
				"buffer_overflow",
				new Object[] {
		    new Integer(outbuf.length),
			new Integer(config.getMTU())});
	}

	return outbuf;
    }

    // Filter the reply to make sure the xid matches and that it's not null.

    static protected boolean
	filterRply(SrvLocMsg msg, SrvLocMsg rply, InetAddress addr) {

	SrvLocHeader mhdr = msg.getHeader();
	SrvLocHeader rhdr = rply.getHeader();

	if (rply == null) {
	    if (config.traceDrop()) {
		config.writeLog("reply_unparsable",
				new Object[] {addr});

	    }

	    return false;

	}

	// Check for invalid xid.
	
	if (mhdr.xid != rhdr.xid) {
	    if (config.traceDrop()) {
		config.writeLog("wrong_xid",
				new Object[] {addr});

	    }
	    return false;

	}
	return true;

    }

    /**
     * Internalize the byte array in the input stream into a SrvLocMsg
     * subclass. It will be an appropriate subclass for the client agent.
     * If an exception comes out of this method, it is converted into
     * a SrvLocMsg with error code.
     *
     *
     * @param dis The input stream containing the packet.
     * @param addr The address of the replying agent (for error reporting).
     * @return The right SrvLocMsg subclass appropriate for the Client Agent.
     *		If null is returned, the function code wasn't recognized,
     *		and so it may be appropriate for another agent.
     * @exception ServiceLocationException If the character set was not valid
     *		or an error occured during parsing.
     * @exception IOException If DataInputStream throws it.
     */

    static protected SrvLocMsg internalize(DataInputStream dis,
					   InetAddress addr)
	throws ServiceLocationException {

	int ver = 0, fun = 0;
	SrvLocMsg msg = null;
	SrvLocHeader hdr = null;
	byte[] b = new byte[2];

	try {

	    dis.readFully(b, 0, 2);

	    ver = (int) ((char)b[0] & 0XFF);
	    fun = (int) ((char)b[1] & 0XFF);

	    // Unrecognized version number if header not returned.

	    if (ver != Defaults.version) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.VERSION_NOT_SUPPORTED,
				"version_number_error",
				new Object[] {new Integer(ver)});

	    }

	    // Create the header. Note that we only need to create a
	    //  client side header here, because that is all that
	    //  will be expected by the client side code. Note that we
	    //  *can't* use the SrvLocHeader.newInstance() mechanism
	    //  because Transact lives in the server as well, and
	    //  SrvLocHeader can only handle one header class per
	    //  version.

	    hdr = new SLPHeaderV2();

	    // Parse header.

	    hdr.parseHeader(fun, dis);

	    // Parse body.

	    if ((msg = hdr.parseMsg(dis)) != null) {

		// Parse options, if any.

		hdr.parseOptions(dis);

	    }

	} catch (IllegalArgumentException ex) {

	    // During parsing, this can be thrown if syntax errors occur.

	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"passthrough_addr",
				new Object[] {ex.getMessage(), addr});

	} catch (IOException ex) {

	    // If version code is zero, then we suspect a network error,
	    //  otherwise, it is probably a parse error.

	    String fcode = (fun == 0 ? "???":Integer.toString(fun));
	    short exCode =
		(ver == 0 ? ServiceLocationException.NETWORK_ERROR:
		 ServiceLocationException.PARSE_ERROR);

	    // During parsing, this can be thrown if the message stream
	    //  is improperly formatted.

	    throw
		new ServiceLocationException(exCode,
					     "ioexception_parsing",
					     new Object[] {
		    ex, fcode, addr, ex.getMessage()});

	} catch (ServiceLocationException ex) {

	    // Add the address of the replying agent.

	    throw
		new ServiceLocationException(ex.getErrorCode(),
					     "passthrough_addr",
					     new Object[] {
		    ex.getMessage(), addr});

	}

	return msg;
    }

    // Send out the message.

    static protected void
	send(DatagramSocket ds, SrvLocMsg msg, InetAddress addr)
	throws ServiceLocationException, IOException {

	byte[] outbuf = getBytes(msg, true, false);
	DatagramPacket dpsend =
	    new DatagramPacket(outbuf, outbuf.length, addr, Defaults.iSLPPort);
	ds.send(dpsend);

    }

    // Check the response and add the previous responder if it is OK.

    static protected boolean
	addPreviousResponder(SrvLocMsg msg, InetAddress addr) {
			
	// Add incoming result to the vector.

	SrvLocHeader hdr = msg.getHeader();
	Vector v = hdr.previousResponders;
	String srcAddr = addr.getHostAddress();

	if (v.contains(srcAddr)) {	// the SRC ignored its PR list
	    if (config.traceDrop()) {
		config.writeLog("drop_pr",
				new Object[] {
		    srcAddr,
			Integer.toHexString(hdr.xid)});

	    }
	    return false;

	} else {
	    hdr.addPreviousResponder(addr);
	    return true;

	}
    }

    // Transact an active request for DA or SA adverts.

    static Vector transactActiveAdvertRequest(ServiceType type,
					      SrvLocMsg rqst,
					      ServerDATable daTable)
	throws ServiceLocationException {

	// Perform active advertisement.

	Vector ret = new Vector();
	Vector results = new Vector();

	// Create Transact object and start.

	Transact tran = new Transact(rqst,
				     results,
				     config.getMulticastTimeouts(),
				     Integer.MAX_VALUE, // config doesn't apply
				     config.getMulticastAddress(),
				     true);

	Thread multiThread = new Thread(tran);

	multiThread.start();

	// Wait until the TransactConverge thread is done, if necessary.

	try {
	    multiThread.join();

	} catch (InterruptedException ex) {

	}

	ServiceLocationException ex = tran.exErr;

	// Report error.

	if (ex != null && config.traceDATraffic()) {
	    config.writeLog("sdat_active_err",
			    new Object[] {new Integer(ex.getErrorCode()),
					      ex.getMessage()});

	    throw ex;

	}

	// Process the results.

	int i, n = results.size();

	for (i = 0; i < n; i++) {
	    Object msg = results.elementAt(i);

	    if ((type.equals(Defaults.DA_SERVICE_TYPE) &&
		!(msg instanceof CDAAdvert)) ||
		(type.equals(Defaults.SA_SERVICE_TYPE) &&
		!(msg instanceof CSAAdvert))) {

		if (config.traceDrop()) {
		    config.writeLog("sdat_nonadvert_err",
				    new Object[] {
			msg});

		}

		continue;
	    }

	    // Let DA table handle it if it`s a DAAdvert.

	    if (type.equals(Defaults.DA_SERVICE_TYPE)) {
		CDAAdvert advert = (CDAAdvert)msg;

		daTable.handleAdvertIn(advert);

	    } else {

		// Add scopes from the SAAdvert if not already there.

		SrvLocHeader hdr = ((SrvLocMsg)msg).getHeader();

		int j, m = hdr.scopes.size();

		for (j = 0; j < m; j++) {
		    Object o = hdr.scopes.elementAt(j);

		    if (!ret.contains(o)) {
			ret.addElement(o);

		    }
		}
	    }
	}

	return ret;
    }

    // Construct a Transact object to run a convergence transaction in
    //  a separate thread.

    Transact(SrvLocMsg msg,
	     Vector ret,
	     int[] msT,
	     int mResults,
	     InetAddress address,
	     boolean continueAfterFound) {

	msgOut = msg;
	returns = ret;
	MSTimeouts = msT;
	maxResults = mResults;
	this.address = address;
	this.continueAfterFound = continueAfterFound;
    }

    // Run the multicast convergence algorithm.

    public void run() {

	Exception xes = null;
	DatagramSocket ds = null;

	// Get the config object if we need it.

	if (config == null) {
	    config = SLPConfig.getSLPConfig();

	}

	// Set thread name.

	if (config.isBroadcastOnly()) {
	    Thread.currentThread().setName("SLP Broadcast Transact");
	    address = config.getBroadcastAddress();

	} else {
	    Thread.currentThread().setName("SLP Multicast Transact");

	}

	try {

	    // Multicast out on the default interface only.

	    ds = config.getMulticastSocketOnInterface(config.getLocalHost(),
						      true);

	    // Perform convergence.

	    transactConvergeMsg(address,
				ds,
				msgOut,
				returns,
				MSTimeouts,
				maxResults,
				continueAfterFound);

	    ds.close();

	    ds = null;

	} catch (ServiceLocationException ex) {

	    // Ignore DA_BUSY, the DA may free up later.

	    if (ex.getErrorCode() != ServiceLocationException.DA_BUSY) {
		exErr = ex;
		xes = ex;

	    }

	} catch (Exception ex) {

	    // Create new exception to be thrown.

	    xes = ex;
	    exErr = new ServiceLocationException(
				ServiceLocationException.INTERNAL_SYSTEM_ERROR,
				"passthrough",
				new Object[] {ex.getMessage()});

	} finally {

	    // Close the socket if it's been opened.

	    if (ds != null) {
		ds.close();

	    }
	}

	// Log any errors.

	if (xes != null) {
	    StringWriter sw = new StringWriter();
	    PrintWriter pw = new PrintWriter(sw);

	    xes.printStackTrace(pw);
	    pw.flush();

	    config.writeLog("multicast_error",
			    new Object[] {xes.getMessage(),
					      sw.toString()});

	}
    }

    /**
     * Send the message using multicast and use convergence to gather the
     * results. Note that this routine must be synchronized because
     * only one multicast can be active at a time; othewise, the client
     * may get back an unexpected result. However, there can be many unicast
     * requests active along with a multicast request, hence the separate
     * thread for multicast.
     *
     * The subtlety of the timing routine is that it will only resend the
     * message when one of the multicast convergence timeout intervals
     * elapses.  Further, for efficiency, it will give up after a complete
     * interval has gone by without receiving any results.  This may mean
     * that the intervals have to be extended in larger networks.  In the
     * common case, multicast convergence will complete under 3 seconds
     * as all results will arrive during the first interval (1 second long)
     * and none will arrive during the second interval.
     *
     * @param     addr  The multicast/broadcast address to send the request to.
     * @param     ds  The datagram socket to send on.
     * @param     msg The message to send.
     * @param     vResult A vector in which to put the returns.
     * @param	msTimeouts Array of timeout values for multicast convergence.
     * @param     maxResults Maximum replies desired.
     * @param	continueAfterFound If true, continue after something is
     *				   found. Try three times if nothing was
     *				   found. If false, exit at the first
     *				   timeout. DA discovery should set this
     *				   to true so as many DAs as possible are
     *				   found, otherwise, it should be false.
     * @exception ServiceLocationException
     *            If results cannot be obtained in the timeout interval
     *            specified in the 'config.' or
     *            if networking resources cannot be obtained or used
     *            effectively.
     */

    static public void
	transactConvergeMsg(InetAddress addr,
			    DatagramSocket ds,
			    SrvLocMsg   msg,
			    Vector vResult,
			    int[] msTimeouts,
			    int maxResults,
			    boolean continueAfterFound)
	throws ServiceLocationException {

	// Get the config object if we need it.

	if (config == null) {
	    config = SLPConfig.getSLPConfig();

	}

	int numReplies = 0;
	int tries = 0;
	SrvLocMsg rply = null;
	ByteArrayOutputStream baos = null;
	int multiMax = config.getMulticastMaximumWait();
	long lStartTime = System.currentTimeMillis();
	int mtu = config.getMTU();

	try {

	    // Send the request for the 1st iteration.  It will be sent again
	    //  only when the timeout intervals elapse.

	    send(ds, msg, addr);
	    tries++;

	    long lTimeSent = System.currentTimeMillis();

	    // Continue collecting results only as long as we need more for
	    //   the 'max results' configuration.

	    while (numReplies < maxResults) {

		// Set up the reply buffer.

		byte [] incoming = new byte[mtu];
		DatagramPacket dprecv =
		    new DatagramPacket(incoming, incoming.length);

		// Block on receive (no longer than max timeout - time spent).

		int iTimeout =
		    getTimeout(lStartTime, lTimeSent, multiMax, msTimeouts);

		if (iTimeout < 0) {
		    break; // we have no time left!
		}

		ds.setSoTimeout(iTimeout);

		try {
		    ds.receive(dprecv);

		} catch (InterruptedIOException ex) {

		    // We try sending at least three times, unless there was
		    // a timeout. If continueAfterFound is false, we exit
		    // after the first timeout if something was found.

		    if ((!continueAfterFound && numReplies > 0) ||
		(int)(System.currentTimeMillis() - lStartTime) > multiMax ||
			tries >= 3) {
			break;

		    }

		    // Now resend the request...

		    send(ds, msg, addr);
		    tries++;
	
		    lTimeSent = System.currentTimeMillis();
		    continue; // since we did not receive anything, continue...

		}

		// Data was received without timeout or fail.

		DataInputStream dis =
		    new DataInputStream(
			new ByteArrayInputStream(dprecv.getData()));

		InetAddress raddr = dprecv.getAddress();
		rply = internalize(dis, raddr);

		if (!filterRply(msg, rply, raddr)) {
		    continue;

		}

		// Add this responder to previous responders. If the message
		//  was already received but the SA resent because it isn't
		//  doing multicast convergence correctly, then ignore it.

		if (!addPreviousResponder(msg, raddr)) {
		    continue;

		}

		// Handle any overflow thru TCP.

		SrvLocHeader rhdr = rply.getHeader();

		if (rhdr.overflow) {

		    rply = transactTCPMsg(raddr, msg, false);

		    if (rply == null) {
			continue;

		    }

		    rhdr = rply.getHeader();
		}

		// Add response to list.

		if (vResult.size() < maxResults) {
		    vResult.addElement(rply);

		}

		// Increment the number of results returned.

		numReplies += rhdr.iNumReplies;

		// Exit if we should not continue.

		if (!continueAfterFound) {
		    break;

		}
	    }
	} catch (ServiceLocationException ex) {

	    // If we broke off because the previous responder's list is too
	    // long, then return, otherwise throw the exception again.

	    if (ex.getErrorCode() ==
		ServiceLocationException.PREVIOUS_RESPONDER_OVERFLOW) {
		return;

	    }

	    throw ex;

	} catch (IOException ex) {

	    throw
		new ServiceLocationException(
				ServiceLocationException.NETWORK_ERROR,
				"ioexception_conv",
				new Object[] {ex, ex.getMessage()});

	}
    }

    // Calculate the multicast timeout depending on where we are in the loop.

    static private int
	getTimeout(long lStart, long lSent, int iTimeout, int[] a_iTOs) {
	int iTotal = (int)(lSent - lStart);

	if (iTimeout < iTotal) {
	    return -1;

	}

	int iWaitTotal = 0;
	int i;

	for (i = 0; i < a_iTOs.length; i++) {
	    iWaitTotal += a_iTOs[i];

	    int iTillNext = (iWaitTotal - iTotal);

	    if (iTotal < iWaitTotal) {
		if (iTimeout < (iTotal + iTillNext)) {
		    return (iTimeout - iTotal);  // max to wait is iTimeout

		} else {
		    return iTillNext; // otherwise wait till next interval
		}
	    }
	}

	return -1; // if we get here we have waited past all of the timeouts
    }

    static {

	config = SLPConfig.getSLPConfig();

    }
}
