MessageUtils.setLocalMessage (msg, MessageUtils.isLocalMessage(failedMsg));
  public synchronized DestinationLink selectLink (Iterator links, AttributedMessage msg,          AttributedMessage failedMsg, int retryCount, Exception last)  {    debug = log.isDebugEnabled();    if (debug) log.debug ("Entered selectLink: msg= " +MessageUtils.toString(msg)+      " failedMsg=" +MessageUtils.toString(failedMsg));    if (debug) {	Message raw = msg.getRawMessage();	log.debug("raw msg = " + raw);	log.debug("raw.getClass() = " + raw.getClass());	/*	if (raw instanceof DirectiveMessage) {	    DirectiveMessage dirMsg = (DirectiveMessage)raw;	    Directive dirs[] = dirMsg.getDirectives();            for (int i=0; i<dirs.length; i++) {		Directive dir = dirs[i];        	log.debug("dir.getClass() = " + dir.getClass());                if (dir instanceof RelayDirective.Change) {		    RelayDirective.Change chg = (RelayDirective.Change)dir;                    log.debug("RelayDirective.Change content = " + chg.getContent());		} else if (dir instanceof DirectiveMessage.DirectiveWithChangeReports) {		    DirectiveMessage.DirectiveWithChangeReports dwcr = (DirectiveMessage.DirectiveWithChangeReports)dir;                    log.debug("DirectiveMessage.DirectiveWithChangeReports  = " + dwcr);                    Directive d = dwcr.getDirective();                    if (d instanceof RelayDirective.Response) {			RelayDirective.Response res = (RelayDirective.Response)d;			log.debug("DirectiveWithChangeReports.getDirective() is RelayDirective.Response response = " + res.getResponse());		    }                    if (d instanceof RelayDirective.Change) {			RelayDirective.Change chg = (RelayDirective.Change)d;			log.debug("DirectiveWithChangeReports.getDirective() is RelayDirective.Change content = " + chg.getContent());		    }                    Collection changes = dwcr.getChangeReports();		    if (changes != null) {			Iterator it = changes.iterator();                        while (it.hasNext()) {			    log.debug("ChangeReport = " + it.next());			}		    }		}				    }	}	*/    }    if (commStartDelaySeconds > 0)    {      //  HACK to attempt to get around nameserver/topology lookup problems      if ((startTime + commStartDelaySeconds*1000) > now()) return null;    }    //  Return if things not right    if (links == null || msg == null) return null;    try    {      if (getNameSupport() == null) return null;      if (getRegistry() == null) return null;    }    catch (Exception e)     {      return null;    }    //  One time initial RTT initializations    if (useRTTService && firstTime)    {      //  Node time      rttService.setInitialNodeTime (initialNodeTime);      //  Comm RTTs      Vector v = new Vector();      while (links.hasNext())      {        DestinationLink link = (DestinationLink) links.next();        if (isOutgoingLink(link) && !isLoopbackLink(link)) v.add (link);      }      DestinationLink l[] = new DestinationLink[v.size()];      l = (DestinationLink[]) v.toArray (l);       for (int i=0; i<l.length; i++)      {        String sendLink = getName (l[i]);        int sendTime = getInitialOneWayTripTime (l[i]);        for (int j=0; j<l.length; j++)        {          String recvLink = convertOutgoingToIncomingLink (getName(l[j]));          int recvTime = getInitialOneWayTripTime (l[j]);          rttService.setInitialCommRTTForLinkPair (sendLink, recvLink, sendTime+recvTime);        }      }      firstTime = false;      return null;  // used up interator    }    //  Handle message attribute transfers in the case of send retries    if (debug) log.debug("msg="+msg+",failedMsg="+failedMsg+",equal?"+(msg==failedMsg));    if (msg != failedMsg && failedMsg != null)     {      //  Transfer known persistent attributes      if (debug) log.debug("MessageUtils.hasMessageNumber(failedMsg)="+MessageUtils.hasMessageNumber(failedMsg));      AgentID id = MessageUtils.getFromAgent(failedMsg);      if (id != null) MessageUtils.setFromAgent(msg, id);       id = MessageUtils.getToAgent(failedMsg);      if (id != null) MessageUtils.setToAgent(msg, id);       if (MessageUtils.hasMessageNumber (failedMsg))      {        //  HACK!  How am I supposed to know which out of all the attributes         //  set by aspects and protocol links to transfer over or not???        MessageUtils.setMessageType (msg, MessageUtils.getMessageType (failedMsg));        MessageNumberingAspect.setMessageNumber (msg, MessageUtils.getMessageNumber (failedMsg));        //MessageUtils.setFromAgent (msg, MessageUtils.getFromAgent (failedMsg));         //MessageUtils.setToAgent (msg, MessageUtils.getToAgent (failedMsg));         MessageUtils.setAck (msg, MessageUtils.getAck (failedMsg));         MessageUtils.setSrcMsgNumber (msg, MessageUtils.getSrcMsgNumber (failedMsg));         MessageUtils.setMessageSize (msg, MessageUtils.getMessageSize (failedMsg));         MessageUtils.setSendTimeout (msg, MessageUtils.getSendTimeout (failedMsg));         MessageUtils.setSendDeadline (msg, MessageUtils.getSendDeadline (failedMsg));       }    }    // copy the links iterator so I can use it more than once    Vector linksV = new Vector();    while (links.hasNext()) {      linksV.add(links.next());    }    //  Handle the special case of local messages      MessageAddress targetAgent = MessageUtils.getTargetAgent (msg);        if (getRegistry().isLocalClient (targetAgent))    {      if (loopbackLink == null)      {        Iterator linksI = linksV.iterator();        while (linksI.hasNext())         {          DestinationLink link = (DestinationLink) linksI.next();           if (isLoopbackLink (link))          {            loopbackLink = link;            break;          }        }      }      if (loopbackLink != null)       {	  return linkChoice (loopbackLink, msg);            }       else       {          // if loopback not loaded, use a regular protocol for local messages 	  loggingService.warn("Missing LoopbackLinkProtocol!");      }    }    //  Get started    String msgString = MessageUtils.toString (msg);    if (debug && !MessageUtils.isNewMessage(msg)) log.debug ("Processing " +msgString);    /**     **  Special case:  Check on a possible message send deadline.  If the message has      **  passed its send deadline we just drop it.     **/    if (MessageUtils.haveSendTimeout(msg) && !MessageUtils.haveSendDeadline(msg))     {      //  Need to set the deadline      int timeout = MessageUtils.getSendTimeout (msg);      if (timeout >= 0) MessageUtils.setSendDeadline (msg, now() + timeout);      else log.warn ("Msg has bad send timeout (" +timeout+ ") (timeout ignored): " +msgString);    }    if (MessageUtils.getSendDeadline (msg) < now())  // default deadline is no deadline    {      //  [Semi-HACK] Declare message as successfully sent so that any incoming      //  acks for it are ignored.  Maybe put this in a dropped msg table instead?      if (log.isInfoEnabled()) log.info ("Dropping msg past its send deadline: " +msgString);       MessageAckingAspect.addSuccessfulSend (msg);      return blackHoleLink;    }    //  Check that the sending agent is a current agent in this node.  If not, it has    //  moved on, and its outstanding (regular) messages with it, so we just drop all    //  but pure ack messages.  As with send deadline above, maybe put this in a dropped     //  msg table instead of the successful sends table?  // TODO Could we get into trouble if the agent returns and still it needs to send this message?// It will be seen as successfully sent here.  Need for dropped msg bucket?  Seems so.  There// is only a small window for this error to take place, so will punt for the moment.    if (!getRegistry().isLocalClient(MessageUtils.getOriginatorAgent(msg)))    {      if (debug) log.debug ("Msg from no-longer-local agent: " +msgString);       if (!MessageUtils.isSomePureAckMessage (msg))      {        if (log.isInfoEnabled()) log.info ("Dropping non-ack msg from no-longer-local agent: " +msgString);         MessageAckingAspect.addSuccessfulSend (msg);  // HACK: needs work - see above        return blackHoleLink;      }    }    //  Lookup up the sending agent of the message in the nameserver.  If it is not registered    //  we return null and try again later, assuming that it will show up soon.  We cannot number    //  the message until we have the incarnation number for the agent - and message numbering    //  occurs after link selection - but here we can cause a wait while down there an exception    //  will need to be thrown.        AgentID fromAgent = MessageUtils.getFromAgent (msg);    if (fromAgent == null)     {      MessageAddress origAgent = null;      try      {        origAgent = MessageUtils.getOriginatorAgent (msg);        fromAgent = AgentID.getAgentID (this, getServiceBroker(), origAgent);      }      catch (Exception e) {}      if (fromAgent == null)      {        if (!MessageUtils.isSomePureAckMessage (msg))  // let acks pass, esp. from now moved agents        {          if (debug) log.debug ("Postponing link selection: null lookup for originator agent: " +origAgent);          return linkChoice (null, msg);        }      }      else MessageUtils.setFromAgent (msg, fromAgent);    }    //  Get the target node for the message.  If this message is a retry/resend, we try to    //  get the latest uncached data because the target node may have changed and thus be    //  the cause of our (real or apparent) send failure.    Ack ack = MessageUtils.getAck (msg);    String targetNode = null;    try    {      if (ack != null && ack.getSendTry() > 0) {        // Retry/resend: bypass topological caching to get latest target agent info/* //102B see new code below        AgentID toAgent = AgentID.getAgentID (this, getServiceBroker(), targetAgent, true);        log.info ("latest toAgent for " +msgString+ ": " +toAgent);            if (log.isInfoEnabled())        {          AgentID oldToAgent = MessageUtils.getToAgent (msg);          if (oldToAgent != null && (!oldToAgent.equals(toAgent)))          {            log.info ("Message re-address: " +MessageUtils.toShortString(msg)+                      "\n  old destination: " +oldToAgent+                      "\n  new destination: " +toAgent);          }        }        MessageUtils.setToAgent (msg, toAgent);        targetNode = toAgent.getNodeName(); */           //102B           // replace commented out code above        // When an agent moves, its node changes, but not its incarnation.  In such         // instances, our message sequences, which number messages between individual        // source and destination pairs based on agent name and incarnation number, remain        // viable across the move.        // But when an agent is restarted, its incarnation number changes, so a new sequence        // needs to be established.  This code deals with the case of outstanding messages -        // a message send to the old location, but for which the send failed or an ack wasn't        // received.  In this case, this message and any other outstanding messages to        // to the restarted agent should be resequenced wrt new agent/incarnation name.        // We also do this when a message is received from an agent with a newer incarnation        // than the one we know about.        AgentID newToAgent = AgentID.getAgentID (this, getServiceBroker(), targetAgent, true);        AgentID oldToAgent = MessageUtils.getToAgent (msg);        if ((oldToAgent != null) && (!oldToAgent.equals(newToAgent))) {          if (oldToAgent.getAgentIncarnation().equals(newToAgent.getAgentIncarnation())) {            // If incarnations are the same, then use new AgentID            if (log.isInfoEnabled()) {              log.info ("Message re-address: " +MessageUtils.toShortString(msg)+                        "\n  old destination: " +oldToAgent+                        "\n  new destination: " +newToAgent);            }            MessageUtils.setToAgent(msg, newToAgent);            targetNode = newToAgent.getNodeName();          } else { // if incs are different, then move all outstanding messages in this sequence to new sequence            if (log.isInfoEnabled()) {              log.info("Destination agent moved to "+newToAgent+" - incarnations different - resequencing msg: "+MessageUtils.toString(msg));            }            // need to worry about synchronization here - message might be cached or in transit             ackSvc.handleMessagesToRestartedAgent(fromAgent, oldToAgent, newToAgent);            // if this msg's toAgent & msgnum wasn't handled by handleMessagesToRestartedAgent, do it here            if (!MessageUtils.getToAgent(msg).equals(newToAgent)) // && !aspect.hasMessageBeenAcked(msg))             {	      // change AgentID and renumber messages              MessageUtils.setToAgent(msg, newToAgent);              numSvc.renumberMessage(msg);            }            if (log.isDebugEnabled())               log.debug("resequenced msg is now: " +MessageUtils.toString(msg));            return null;  // this message is returned to queue to be reprocessed now that its AgentID has been updated            //MessageAckingAspect.addSuccessfulSend (msg);            //return blackHoleLink;  // causes msg to be forwarded to a link that doesn't do anything but return success.                                       // only issue might be what other aspects seeing success returned might do -                                     // we don't want them sticking this message in a table somewhere          }        } else {          targetNode = oldToAgent.getNodeName();        }      }      else      {        //  New message: cached info is good enough for now        targetNode = MessageUtils.getToAgentNode (msg);        if (targetNode == null)        {          //  Get cached target agent info          //104B temporary hack for WP messages          AgentID toAgent;          //104B try {             toAgent = AgentID.getAgentID (this, getServiceBroker(), targetAgent, false);          //104B } catch (NameLookupException nle) {            //104B Message rawMsg = msg.getRawMessage();            //104B if (rawMsg instanceof WhitePagesMessage) {              //104B String s = targetAgent.getAddress();              //104B toAgent = new AgentID (s, s, "0");            //104B } else {              //104B throw nle;            //104B }          //104B }          MessageUtils.setToAgent (msg, toAgent);          targetNode = toAgent.getNodeName();          log.info ("toAgent for new msg " +msgString+ ": " +toAgent);        }      }    }    catch (Exception e)     {      if (log.isDebugEnabled()) {        log.debug("exception getting toAgent for " +msgString+ ": ", e);      } else if (log.isInfoEnabled()) {        log.info("exception getting toAgent for " +msgString);      }    }    //  Cannot continue without knowing the name of the target node for the message    if (targetNode == null)     {      if (debug) log.debug ("Postponing link selection: unknown node for target agent: " +targetAgent);      return linkChoice (null, msg);    }    //  Normal operation.    //    //  First filter the given links into a vector of outgoing links that are able    //  to send the given message.    //    //  NOTE:  Link selection must insure that the link finally returned is a one     //  of these filtered links, as they are the only valid links for this iteration.    Vector v = new Vector();    Iterator linksI = linksV.iterator();    while (linksI.hasNext())     {      DestinationLink link = null;      int cost;      try      {        link = (DestinationLink) linksI.next(); if (debug) log.debug("link =" + link.getProtocolClass());        //  Obvious filters        if (isLoopbackLink (link)) continue;        if (!isOutgoingLink (link)) continue;        //  Cost related filtering        cost = getLinkCost (link, msg);if (debug) log.debug("cost =" + cost);        if (cost == Integer.MAX_VALUE) continue;         if (cost == 1 && getName(link).equals ("org.cougaar.core.mts.SSLRMILinkProtocol"))        {          return linkChoice (link, msg);  //  Temp HACK for UC3        }        //  Drop links that cannot handle messages beyond a certain size        if (getName(link).equals ("org.cougaar.core.mts.udp.OutgoingUDPLinkProtocol"))        {          int msgSize = MessageUtils.getMessageSize (msg);          if (msgSize >= OutgoingUDPLinkProtocol.getMaxMessageSizeInBytes()) continue;        }        else if (getName(link).equals ("org.cougaar.core.mts.email.OutgoingEmailLinkProtocol"))        {          int msgSize = MessageUtils.getMessageSize (msg);          if (msgSize >= OutgoingEmailLinkProtocol.getMaxMessageSizeInBytes()) continue;        }        //  NOTE: As of 8.6.1 an odd side effect of calling link.cost() above is to do the         //  equivalent of a call to canSendMessage(), so we comment it out here for now.        //  if (!canSendMessage (protocolClass, msg)) continue;      }      catch (Exception e)      {        e.printStackTrace();        continue;      }      if (link != null) v.add (link);    }    if (ack != null) ack.setNumberOfLinkChoices (v.size());    //  No able outgoing links    if (v.size() == 0) {      if (debug) log.debug ("Postponing link selection: no able outgoing links to " +targetAgent);      return linkChoice (null, msg);    }    //  Rank the links based on the chosen (via property) metric    DestinationLink destLinks[] = (DestinationLink[]) v.toArray (new DestinationLink[v.size()]);     linkRanker.setMessage (msg);    Arrays.sort (destLinks, linkRanker);    DestinationLink topLink = destLinks[0];    //  Special Case:  Sending pure ack messages.  For pure ack messages we try all possible     //  links until we run out or we determine we no longer need to send the pure ack.    if (MessageUtils.isPureAckMessage (msg))    {      PureAck pureAck = (PureAck) ack;      if (debug) log.debug ("Choosing link for pure ack msg: " +msgString);      //  First, add all the links that have now piggybacked the ack to our used list.      //  If there are any, then we may not be needing to send a pure ack this time - we may      //  wait till sometime after the farthest out response to these sends is expected.      DestinationLink link;      long latestDeadline = 0;      for (int i=0; i<destLinks.length; i++)      {        link = destLinks[i];        if (pureAck.haveLinkSelection (link)) continue;        long lastSendTime = MessageAckingAspect.getLastSendTime (link, targetNode);        if (lastSendTime > pureAck.getAckSendableTime())        {          boolean newLink = pureAck.addLinkSelection (link);          if (newLink)          {            //  Ok, here's a link that we'll not be needing to send an ack over because            //  its already been done for us.  Update the latestDeadline for when we            //  need to attempt to send this ack over another link.            if (debug) log.debug ("Dropping used " +getName(link)+ " for " +msgString);            int rtt = rttService.getBestFullRTTForLink (link, targetNode);            if (rtt <= 0) rtt = getLinkCost (link, msg);            float spacingFactor = MessageAckingAspect.getInterAckSpacingFactor();            long deadline = lastSendTime + (long)((float)rtt * spacingFactor);            if (deadline > latestDeadline) latestDeadline = deadline;          }        }      }      //  Check if we have exhusted all the links for sending the ack      if (pureAck.getNumberOfLinkSelections() == destLinks.length)      {        if (log.isInfoEnabled()) log.info ("No untried links left to send pure ack, dropping it: " +msgString);        return blackHoleLink;      }      //  Possibly reschedule the pure ack msg for sending later      if (latestDeadline > now())      {        if (debug)         {          long t = latestDeadline - now();          log.debug ("Rescheduling pure ack msg with timeout of " +t+ ": " +msgString);        }        pureAck.setSendDeadline (latestDeadline);        MessageAckingAspect.addToPureAckSender ((PureAckMessage)msg);        return blackHoleLink;      }      //  If we get here, it is time to try to send the ack over the highest ranking      //  link left that has not had the ack already go over it.      for (int i=0; i<destLinks.length; i++)      {        link = destLinks[i];        if (!pureAck.haveLinkSelection (link))         {          pureAck.addLinkSelection (link);          return linkChoice (link, msg);        }      }      log.info("No untried able links for sending this pure ack. Dropping pure ack. Correspondent will resend. " +msgString);      return blackHoleLink;    }    //  Special Case:  Sending pure ack-acks.    if (MessageUtils.isPureAckAckMessage (msg))    {      //  Link selection for these messages is limited to whatever link was last used      //  successfully to its target node (if that link is currently available).      //  NOTE:  Needs work.      Class linkClass = MessageAckingAspect.getLastSuccessfulLinkUsed (targetNode);      DestinationLink link = pickLinkByClass (destLinks, linkClass);      if (link == null) link = topLink;  // linkClass may not be currently available      return linkChoice (link, msg);    }    //  Special Case:  Sending traffic masking messages.    if (MessageUtils.isTrafficMaskingMessage (msg))    {      //  Link selection for these messages is limited to whatever link was last used      //  successfully to its target node (if that link is currently available).      //  This is just preliminary support for traffic masking messages.           if (ack == null || (ack.getSendTry() == 0 && ack.getSendCount() == 0))      {         Class linkClass = MessageAckingAspect.getLastSuccessfulLinkUsed (targetNode);        DestinationLink link = pickLinkByClass (destLinks, linkClass);        if (link == null) link = topLink;  // linkClass may not be currently available        return linkChoice (link, msg);      }      else      {        //  For now, if there are send failures, just drop masking messages, as        //  we don't need to be clogging up the pipes with chaff at this time.        if (log.isInfoEnabled()) log.info ("Send failure with masking msg, dropping it: " +msgString);        return blackHoleLink;      }    }/*was new, now old; should be deleted soon    //  Special Case:  Message retrying (first send attempt generated an exception)    //  or resending (message did not get an ack).  What's special here is that we avoid     //  (if possible) choosing any link that the message has already tried and actually    //  or apparently failed with.  HACK!    if (ack != null && (ack.getSendTry() > 0 || ack.getSendCount() > 0))    {      //  First try the last link successfully used to send a message to this destination      Class linkClass = MessageAckingAspect.getLastSuccessfulLinkUsed (targetNode);      DestinationLink link = pickLinkByClass (destLinks, linkClass);      boolean newLink = (link != null ? ack.addLinkSelection (link) : false);       if (newLink)      {        if (debug) log.debug ("Chose last successful link for retry/resend of " +msgString);        return linkChoice (link, msg);      }      //  Otherwise choose the highest ranking transport link not yet chosen (if any)      link = null;      for (int i=0; i<destLinks.length; i++)      {        if (!ack.haveLinkSelection (destLinks[i]))         {          link = destLinks[i];          break;        }      }      //  Tried them all      if (link == null)      {        if (ack.getSendCount() == 0)  // Retry        {          //  If we still don't have a choice at this point, we clear all our previous          //  selections and return null.  This allows the retry to slow down (there          //  will be an increasing delay with each null), and start the link selection          //  cycle anew when the message comes back thru again.          if (debug) log.debug ("Exhausted retry link choices; will cycle thru again after delay");          ack.clearLinkSelections();          link = null;        }        else  // Resend        {          //  Since resends are managed by the acking message resender, including their          //  timing with any delays, we start the link recycling now and don't induce          //  any extra delay here like with retries.          if (debug) log.debug ("Exhausted resend link choices; starting recycle now");          link = pickLinkByClass (destLinks, ack.getFirstLinkSelection());          if (link == null) link = topLink;          ack.clearLinkSelections();        }      }      if (link != null) ack.addLinkSelection (link);      return linkChoice (link, msg);    }*/    /**     *  Main Selection Algorithm     *     *  Choose a link based on its metric, history of sending messages, and policy (tbd). The     *  algorithm here now could be extended with more message send & receive history, link     *  metric data, and Cougaar Policy.    **/    //  Get the last link choice made for the destination node for this message    SelectionChoice lastChoice = (SelectionChoice) selectionHistory.get (targetNode);//log.debug ("last choice = " + lastChoice);//if (lastChoice != null) log.debug ("last choice was successful = " + lastChoice.wasSuccessful());    //  Special Case:  First time a message is sent to this destination    //  or there is only one link to choose from.  In these cases we always     //  choose the top/only link.    if (lastChoice == null || destLinks.length == 1)    {      return linkChoice (setSteadyStateChoice (lastChoice, topLink, msg), msg);    }    //  If the last choice was successful, we need to choose between keeping the    //  same choice, or trying to upgrade (i.e. get a higher ranking transport).      if (lastChoice.wasSuccessful())    {      //  Preliminary work:  If the last choice was an upgrade or a replacement choice,      //  then good news!  It worked!  We then transform that choice into our      //  new steady state case, and go on from there.      if (lastChoice.wasUpgradeChoice() || lastChoice.wasReplacementChoice())      {        lastChoice.transformChoiceToSteadyStateChoice();      }      //  Update steady choice link selection count for target node      int count = getTopLinkSteadyStateCountForNode (targetNode);      if (++count >= tryOtherLinksInterval) count = 0;      setTopLinkSteadyStateCountForNode (targetNode, count);      //  We've used this link so many times in a row now, it may be time      //  to consider other links that may have improved, or never been tried.      if (count == 0)      {//log.debug ("\n** trying OTHER LINKS");        //  Look for the highest ranking link left that still is lacking a minimum number of        //  data points in its average commRTT measurement.        float MIN_FILL = 0.5f;  // HACK: arbitrary for now        for (int i=0; i<destLinks.length; i++)        {          if (rttService.getHighestCommRTTPercentFilled (destLinks[i], targetNode) < MIN_FILL)           {//log.debug ("\n** chose link still lacking a minimum number of data points in commRTT average");            return linkChoice (setReplacementChoice (lastChoice, destLinks[i], msg), msg);          }        }//log.debug ("\n** !!all links above the minimum number of data points in commRTT average");        //  Otherwise, choose the link that was last chosen the longest ago that        //  is currently available/valid.        DestinationLink oldestLink = topLink;        long oldestTime = Long.MAX_VALUE;        Hashtable linkSelections = getLinkSelections (targetNode);        for (Enumeration e=linkSelections.elements(); e.hasMoreElements(); )        {          LinkSelectionRecord rec = (LinkSelectionRecord) e.nextElement();          if (!isMemberOf (destLinks, rec.link)) continue;  // link not currently available          if (rec.selectionTime < oldestTime)          {            oldestLink = rec.link;            oldestTime = rec.selectionTime;          }        }//log.debug ("\n** chose (currenly valid) link last chosen longest ago");        return linkChoice (setReplacementChoice (lastChoice, oldestLink, msg), msg);      }      //  Special Case:  At this point, if last choice is the top link, keep using it      if (lastChoice.getLink() == topLink)      {          return linkChoice (setSteadyStateChoice (lastChoice, topLink, msg), msg);      }      //  Decide whether or not to try to upgrade.  An upgrade is by definition      //  a higher ranking transport than we are currently using.      DestinationLink upgrade = null;      int numSends = lastChoice.getNumSteadyStateSends();      if (numSends >= minSteadyState)      {        int sendMetric = getMetric (lastChoice.getSteadyStateLink(), msg);        int topMetric = getMetric (topLink, msg);        if (numSends*sendMetric > upgradeMetricMultiplier*topMetric)        {          //  We've exceeded the metric of N (default 10) sends by the top link.          //  Time to think about a better route man.          upgrade = pickUpgradeChoice (lastChoice, destLinks, msg);        }      }      if (upgrade != null)      {        return linkChoice (setUpgradeChoice (lastChoice, upgrade, msg), msg);      }      else  // not yet time to upgrade or no upgrade available      {        // Keep on keeping on with what we got for now, if it is still valid        if (isMemberOf (destLinks, lastChoice.getLink()))        {          return linkChoice (setSteadyStateChoice (lastChoice, lastChoice.getLink(), msg), msg);        }        else // go ahead and jump to the top link        {          return linkChoice (setUpgradeChoice (lastChoice, topLink, msg), msg);        }      }    }    //  The last choice was not successful    //  We are still in the game.  We respond based on the category the last    //  choice falls into (upgrade, steady state, or replacement choice).    if (lastChoice.wasUpgradeChoice())    {      //  Ok, so our upgrade failed.  It was worth a shot!  Our 2 choices at this point      //  are to try another upgrade, or to go back to the tried and true steady state.      //  For now we will try some number of upgrades before we go back to steady state      DestinationLink upgrade = null;      if (lastChoice.getNumUpgradeTries() < maxUpgradeTries)        {        upgrade = pickUpgradeChoice (lastChoice, destLinks, msg);      }      if (debug) log.debug ("Upgrade choice failed: new upgrade= " +upgrade);      if (upgrade != null)      {        return linkChoice (setUpgradeChoice (lastChoice, upgrade, msg), msg);      }      else  // exceeded # of tries, or no upgrade available      {        //  Go back to steady state, if we can        if (isMemberOf (destLinks, lastChoice.getSteadyStateLink()))        {          return linkChoice (setSteadyStateChoice (lastChoice, lastChoice.getSteadyStateLink(), msg), msg);        }        else  // surest bet is to get a replacement (an upgrade may not be available)        {          DestinationLink replacement = pickReplacementChoice (lastChoice, destLinks, msg);          return linkChoice (setReplacementChoice (lastChoice, replacement, msg), msg);        }      }    }    else if (lastChoice.wasSteadyStateChoice())    {      //  Uh-oh, our old reliable (at least one message successfully sent!) steady       //  state has had anything from a hickup to a coronary.  We basically have 2 choices      //  here - retry the steady state, or choose a replacement link (that may be higher      //  or lower ranked, but will be different than the steady state, unless the      //  steady state is the only link left in town to try).      //  For now we will always try a replacement choice      DestinationLink replacement = pickReplacementChoice (lastChoice, destLinks, msg);      return linkChoice (setReplacementChoice (lastChoice, replacement, msg), msg);    }    else if (lastChoice.wasReplacementChoice())    {      //  Hmmmm, our replacement choice did not work out.  So we continue to try another      //  link that we haven't tried lately.  In the case of repeated failures      //  here, we end up continually cycling through all of the available links.      DestinationLink replacement = pickReplacementChoice (lastChoice, destLinks, msg);      return linkChoice (setReplacementChoice (lastChoice, replacement, msg), msg);    }    else    {      //  What?  How did we get here?  Make a note of it and move on...      loggingService.error ("Invalid state reached! (but we can deal with it)");      DestinationLink replacement = pickReplacementChoice (lastChoice, destLinks, msg);      return linkChoice (setReplacementChoice (lastChoice, replacement, msg), msg);    }  }