Package com.aelitis.azureus.plugins.net.buddy

Source Code of com.aelitis.azureus.plugins.net.buddy.BuddyPluginBuddy$buddyMessage

/*
* Created on Apr 1, 2008
* Created by Paul Gardner
*
* Copyright 2008 Vuze, Inc.  All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License only.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*/


package com.aelitis.azureus.plugins.net.buddy;

import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;

import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DelayedEvent;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.plugins.messaging.MessageException;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageConnection;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageConnectionListener;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageEndpoint;
import org.gudy.azureus2.plugins.messaging.generic.GenericMessageRegistration;
import org.gudy.azureus2.plugins.utils.PooledByteBuffer;
import org.gudy.azureus2.plugins.utils.security.SEPublicKey;
import org.gudy.azureus2.plugins.utils.security.SEPublicKeyLocator;
import org.gudy.azureus2.plugins.utils.security.SESecurityManager;

import com.aelitis.azureus.core.util.AZ3Functions;


public class
BuddyPluginBuddy
{
  private static final boolean TRACE = BuddyPlugin.TRACE;
 
  private static final int CONNECTION_IDLE_TIMEOUT  = 5*60*1000;
  private static final int CONNECTION_KEEP_ALIVE    = 1*60*1000;
 
  private static final int MAX_ACTIVE_CONNECTIONS    = 5;
  private static final int MAX_QUEUED_MESSAGES    = 256;
 
  private static final int RT_REQUEST_DATA  = 1;
 
  private static final int RT_REPLY_DATA    = 2
  private static final int RT_REPLY_ERROR    = 99;

 
  private BuddyPlugin    plugin;
  private long      created_time;
  private int        subsystem;
  private boolean      authorised;
  private String      public_key;
  private String      nick_name;
  private List<Long>    recent_ygm;
 
  private int        last_status_seq;
 
  private long      post_time;
  private InetAddress    ip;
  private int        tcp_port;
  private int        udp_port;
  private int        online_status  = BuddyPlugin.STATUS_ONLINE;  // default
   
  private int        version    = BuddyPlugin.VERSION_CHAT;  // assume everyone now supports chat
 
  private boolean      online;
  private long      last_time_online;
 
  private long      status_check_count;
  private long      last_status_check_time;
 
  private boolean      check_active;
   
  private List<buddyConnection>    connections  = new ArrayList<buddyConnection>();
  private List<buddyMessage>      messages  = new ArrayList<buddyMessage>();
  private buddyMessage        current_message;
 
  private int  next_connection_id;
  private int  next_message_id;
 
  private boolean  ygm_active;
  private boolean  ygm_pending;
 
  private long   latest_ygm_time;
  private String  last_message_received;
 
  private Set<Long>    offline_seq_set;
 
  private int    message_out_count;
  private int    message_in_count;
  private int    message_out_bytes;
  private int    message_in_bytes;
 
  private String  received_frag_details = "";
 
  private BuddyPluginBuddyMessageHandler    persistent_msg_handler;

  private Map<Object,Object>    user_data = new LightHashMap<Object,Object>();
 
  private boolean  keep_alive_outstanding;
  private volatile long  last_connect_attempt  = SystemTime.getCurrentTime();
  private volatile int  consec_connect_fails;
 
  private long last_auto_reconnect  = -1;
 
  private Object        rss_lock = new Object();
 
  private Set<String>      rss_local_cats;
  private Set<String>      rss_remote_cats;
  private Set<String>      rss_cats_read;
 
  private AESemaphore      outgoing_connect_sem = new AESemaphore( "BPB:outcon", 1 );
 
  private volatile boolean  closing;
  private volatile boolean  destroyed;
 
  protected
  BuddyPluginBuddy(
    BuddyPlugin  _plugin,
    long    _created_time,
    int      _subsystem,
    boolean    _authorised,
    String    _pk,
    String    _nick_name,
    int      _version,
    String    _rss_local_cats,
    String    _rss_remote_cats,
    int      _last_status_seq,
    long    _last_time_online,
    List<Long>  _recent_ygm )
  {
    plugin        = _plugin;
    created_time    = _created_time;
    subsystem      = _subsystem;
    authorised      = _authorised;
    public_key       = _pk;
    nick_name      = _nick_name;
    version        = Math.max( version, _version );
    rss_local_cats    = stringToCats( _rss_local_cats );
    rss_remote_cats    = stringToCats( _rss_remote_cats );
    last_status_seq    = _last_status_seq;
    last_time_online  = _last_time_online;
    recent_ygm      = _recent_ygm;
   
    persistent_msg_handler = new BuddyPluginBuddyMessageHandler( this, new File(plugin.getBuddyConfigDir(), public_key ));
  }
 
  protected void
  setInitialStatus(
    long  now,
    int    num_buddies )
  {
      // for inactive buddies we schedule their status checks so that on average we don't
      // do more than one check every 5 minutes
   
    if (   last_time_online == 0 &&
        now - created_time > 7*24*60*60*1000L ){
     
      last_status_check_time = now + RandomUtils.nextInt( 5*60*1000 * num_buddies );
    }
  }
 
  protected BuddyPlugin
  getPlugin()
  {
    return( plugin );
  }
 
  public BuddyPluginBuddyMessageHandler
  getMessageHandler()
  {
    return( persistent_msg_handler );
  }
 
  protected void
  persistentDispatchPending()
  {
    plugin.persistentDispatchPending( this );
  }
 
  protected void
  checkPersistentDispatch()
  {
    persistent_msg_handler.checkPersistentDispatch();
  }
 
  protected void
  persistentDispatch()
  {
    persistent_msg_handler.persistentDispatch();
  }
 
  public Map
  readConfigFile(
    File    name )
  {
    return( plugin.readConfigFile( name ));
  }
 
  public boolean
  writeConfigFile(
    File    name,
    Map      data )
  {
    return( plugin.writeConfigFile( name, data ));
  }
 
  protected long
  getCreatedTime()
  {
    return( created_time );
  }
 
  public int
  getSubsystem()
  {
    return( subsystem );
  }
 
  protected void
  setSubsystem(
    int    _s )
  {
    subsystem = _s;
  }
 
  public boolean
  isAuthorised()
  {
    return( authorised );
  }
 
  protected void
  setAuthorised(
    boolean    _a )
  {
    authorised = _a;
  }
 
  public String
  getPublicKey()
  {
    return( public_key );
  }
 
  protected byte[]
  getRawPublicKey()
  {
    return( Base32.decode( public_key ));
  }
 
  protected String
  getShortString()
  {
    return( public_key.substring( 0, 16 ) + "..." );
  }
 
  public String
  getNickName()
  {
    return( nick_name );
  }
 
  public int
  getVersion()
  {
    return( version );
  }
 
  protected void
  setVersion(
    int    v )
  {
    if ( version < v ){
     
      version = v;
     
      plugin.fireDetailsChanged( this );
    }
  }
 
  public String
  getLocalAuthorisedRSSCategoriesAsString()
  {
    synchronized( rss_lock ){
   
      return( catsToString( rss_local_cats ));
    }
  }
 
  public Set<String>
    getLocalAuthorisedRSSCategories()
    {
    synchronized( rss_lock ){
     
      return( rss_local_cats );
    }
    }
 
  public void
  addLocalAuthorisedRSSCategory(
    String  category )
  {
    category = plugin.normaliseCat( category );
 
    boolean dirty;
   
    synchronized( rss_lock ){
     
      if ( rss_local_cats == null ){
       
        rss_local_cats = new HashSet<String>();
      }
     
      if ( dirty = !rss_local_cats.contains( category )){
             
        rss_local_cats.add( category );
      }
    }
   
    if ( dirty ){
     
      plugin.setConfigDirty();
     
      plugin.fireDetailsChanged( this );
     
        // tell buddy of change
     
      if ( isConnected()){
       
        sendKeepAlive();
      }
    }
  }
 
  public void
  removeLocalAuthorisedRSSCategory(
    String  category )
  {
    category = plugin.normaliseCat( category );
   
    boolean  dirty;
   
    synchronized( rss_lock ){
     
      if ( rss_local_cats == null ){
     
        return;
     
      }else{
   
        dirty = rss_local_cats.remove( category );
      }
    }
   
    if ( dirty ){
     
      plugin.setConfigDirty();
   
      plugin.fireDetailsChanged( this );
     
        // tell buddy of change
     
      if ( isConnected()){
       
        sendKeepAlive();
      }
    }
  }
 
  public void
  setLocalAuthorisedRSSCategories(
    String      new_cats )
  {
    setLocalAuthorisedRSSCategories( stringToCats( new_cats ));
  }
 
  public void
  setLocalAuthorisedRSSCategories(
    Set<String>    new_cats )
  { 
    plugin.normaliseCats( new_cats );
   
    boolean dirty;
   
    synchronized( rss_lock ){
     
      if ( dirty = !catsIdentical( new_cats, rss_local_cats) ){
     
        rss_local_cats = new_cats;
      }
    }
   
    if ( dirty ){
     
      plugin.setConfigDirty();
     
      plugin.fireDetailsChanged( this );
     
        // tell buddy of change
     
      if ( isConnected()){
       
        sendKeepAlive();
      }
    } 
  }
 
  public Set<String>
    getRemoteAuthorisedRSSCategories()
    {
      return( rss_remote_cats );
    }
 
  public String
  getRemoteAuthorisedRSSCategoriesAsString()
  {
    return( catsToString( rss_remote_cats ));
  }
 
  protected void
  setRemoteAuthorisedRSSCategories(
    Set<String>    new_cats )
  {
    plugin.normaliseCats( new_cats );
   
    boolean  dirty;
   
    synchronized( rss_lock ){
     
      if ( dirty = !catsIdentical( new_cats, rss_remote_cats) ){
     
        rss_remote_cats = new_cats;
      }
    }
   
    if ( dirty ){
     
      plugin.setConfigDirty();
      
      plugin.fireDetailsChanged( this );
    }
  }
 
  public boolean
  isLocalRSSCategoryAuthorised(
    String  category )
  {
    category = plugin.normaliseCat( category );
 
    synchronized( rss_lock ){
     
      if ( rss_local_cats != null ){
     
        return( rss_local_cats.contains( category ));
      }
   
      return( false );
    }
  }
 
  public boolean
  isRemoteRSSCategoryAuthorised(
    String  category )
  {
    category = plugin.normaliseCat( category );
   
    synchronized( rss_lock ){
     
      if ( rss_remote_cats != null ){
     
        return( rss_remote_cats.contains( category ));
      }
     
      return( false );
    }
  }
 
  protected void
  localRSSCategoryRead(
    String    str )
  {
    boolean dirty;
   
    synchronized( rss_lock ){
     
      if ( rss_cats_read == null ){
       
        rss_cats_read = new HashSet<String>();
      }
     
      dirty = rss_cats_read.add( str );
    }
   
    if ( dirty ){
     
      // not persisted currently - plugin.setConfigDirty();
      
      plugin.fireDetailsChanged( this );
    }
  }
 
  public String
  getLocalReadCategoriesAsString()
  {
    synchronized( rss_lock ){

      return( catsToString( rss_cats_read ));
    }
  }
 
  public URL
  getSubscriptionURL(
    String    cat )
  {
    String url = "azplug:?id=azbuddy&name=Friends&arg=";
   
    String arg = "pk=" + getPublicKey() + "&cat=" + cat;

    try{
      url += URLEncoder.encode( arg, "UTF-8" );
     
      return( new URL( url ));
     
    }catch( Throwable e ){
 
      Debug.out( e );
     
      return( null );
    }
  }
 
  public void
  subscribeToCategory(
    String  cat )
 
    throws BuddyPluginException
  {
    AZ3Functions.provider az3 = AZ3Functions.getProvider();

    if ( az3 == null ){
     
      throw( new BuddyPluginException( "AZ3 subsystem not available" ));
    }
   
    try{
      az3.subscribeToRSS(
        getName() + ": " + cat,
        getSubscriptionURL(cat),
        15,
        false,
        getPublicKey() + ":" + cat );
       
    }catch( Throwable e ){
       
      throw( new BuddyPluginException( "Failed to add subscription", e ));
    }
  }
 
  public boolean
  isSubscribedToCategory(
    String  cat,
    String  creator_ref )
  {
    if ( creator_ref == null ){
     
      return( false );
    }
   
    return( creator_ref.equals( getPublicKey() + ":" + cat ));
  }
 
  protected String
  catsToString(
    Set<String>  cats )
  {
    if ( cats == null || cats.size() == 0 ){
     
      return( null );
    }
   
    String  str = "";
   
    for (String s:cats ){
     
      str += (str.length()==0?"":",") + s;
    }
   
    return( str );
  }
 
  protected boolean
  catsIdentical(
    Set<String>  c1,
    Set<String>  c2 )
  {
    if ( c1 == null && c2 == null ){
     
      return( true );
     
    }else if ( c1 == null || c2 == null ){
     
      return( false );
     
    }else{
     
      return( c1.equals( c2 ));
    }
  }
 
  protected Set<String>
  stringToCats(
    String  str )
  {
    if ( str == null ){
     
      return( null );
    }
   
    String[] bits = str.split( "," );
   
    Set<String> res = new HashSet<String>( bits.length );
   
    for ( String b: bits ){
     
      b = b.trim();
     
      if ( b.length() > 0 ){
       
        res.add( b );
      }
    }
   
    if ( res.size() == 0 ){
     
      return( null );
    }
   
    return( res );
  }
 
  public int
  getOnlineStatus()
  {
    return( online_status );
  }
 
  protected void
  setOnlineStatus(
    int    s )
  {
    if ( online_status != s ){
     
      online_status = s;
     
      plugin.fireDetailsChanged( this );
    }
  }
 
  public String
  getName()
  {
    if ( nick_name != null ){
     
      return( nick_name );
    }
   
    return( getShortString());
  }
 
  public void
  remove()
  {
    persistent_msg_handler.destroy();
   
    plugin.removeBuddy( this );
  }
 
  public InetAddress
  getIP()
  {
    return( ip );
  }
 
  public InetAddress
  getAdjustedIP()
  {
    if ( ip == null ){
     
      return( null );
    }
   
    InetSocketAddress address = new InetSocketAddress( ip, tcp_port );
   
    InetSocketAddress adjusted_address = AddressUtils.adjustTCPAddress( address, true );
   
    if ( adjusted_address != address ){
     
      return( adjusted_address.getAddress());
    }
   
    address = new InetSocketAddress( ip, udp_port );
   
    adjusted_address = AddressUtils.adjustUDPAddress( address, true );
   
    if ( adjusted_address != address ){
     
      return( adjusted_address.getAddress());
    }
 
    return( ip );
  }
 
  public List
  getAdjustedIPs()
  {
    List  result = new ArrayList();
   
    if ( ip == null ){
     
      return( result );
    }
   
    InetAddress adjusted = getAdjustedIP();
   
    if ( adjusted == ip ){
     
      result.add( ip );
     
    }else{
     
      List l = AddressUtils.getLANAddresses( adjusted.getHostAddress());
     
      for (int i=0;i<l.size();i++){
       
        try{
          result.add( InetAddress.getByName((String)l.get(i)));
         
        }catch( Throwable e ){
         
        }
      }
    }
   
    return( result );
  }

 
  public int
  getTCPPort()
  {
    return( tcp_port );
  }
 
  public int
  getUDPPort()
  {
    return( udp_port );
  }
 
  public boolean
  isOnline(
    boolean  is_connected )
  {
    boolean  connected = isConnected();
   
      // if we're connected then we're online whatever
   
    if ( connected ){
     
      return( true );
    }
   
    if ( !online ){
     
      return( false );
    }
 
    if ( is_connected ){
   
      return( false );
     
    }else{
     
      return( true );
    }
  }
 
  protected boolean
  isIdle()
  {
    synchronized( this ){
   
      return( connections.size() == 0 );
    }
  }
 
  public long
  getLastTimeOnline()
  {
    return( last_time_online );
  }
 
  public BuddyPlugin.cryptoResult
  encrypt(
    byte[]    payload )
 
    throws BuddyPluginException
  {
    return( plugin.encrypt( this, payload ));
  }
 
  public BuddyPlugin.cryptoResult
  decrypt(
    byte[]    payload )
 
    throws BuddyPluginException
  {
    return( plugin.decrypt( this, payload, getName() ));
  }
 
  public boolean
  verify(
    byte[]    payload,
    byte[]    signature )
 
    throws BuddyPluginException
  {
   
    return( plugin.verify( this, payload, signature ));
  }
 
  public BuddyPluginBuddyMessage
  storeMessage(
    int    type,
    Map    msg )
  {
    return( persistent_msg_handler.storeExplicitMessage( type, msg ));
  }
 
  public List<BuddyPluginBuddyMessage>
  retrieveMessages(
    int    type )
  {
    return( persistent_msg_handler.retrieveExplicitMessages( type ));
  }
 
  public void
  setMessagePending()
 
    throws BuddyPluginException
  {
    synchronized( this ){
     
      if ( ygm_active ){
       
        ygm_pending = true;
       
        return;
      }
     
      ygm_active = true;
    }
   
    plugin.setMessagePending(
      this,
      new BuddyPlugin.operationListener()
      {
        public void
        complete()
        {
          boolean  retry;
         
          synchronized( BuddyPluginBuddy.this ){
           
            ygm_active = false;
           
            retry = ygm_pending;
           
            ygm_pending = false;
          }
         
          if ( retry ){
           
            try{
              setMessagePending();
             
            }catch( BuddyPluginException e ){
             
              log( "Failed to send YGM", e );
            }
          }
        } 
      });
  }
 
  public long
  getLastMessagePending()
  {
    return( latest_ygm_time );
  }
 
  protected boolean
  addYGMMarker(
    long    marker )
  {
    Long  l = new Long( marker );
   
    synchronized( this ){
   
      if ( recent_ygm == null ){
       
        recent_ygm = new ArrayList<Long>();
      }
     
      if ( recent_ygm.contains( l )){
       
        return( false );
      }
     
      recent_ygm.add( l );
     
      if ( recent_ygm.size() > 16 ){
       
        recent_ygm.remove(0);
      }
     
      latest_ygm_time = SystemTime.getCurrentTime();
    }
   
    plugin.setConfigDirty();
   
    plugin.fireDetailsChanged( this );
   
    return( true );
  }
 
  protected void
  setLastMessageReceived(
    String    str )
  {
    last_message_received = str;
   
    plugin.fireDetailsChanged( this );
  }
 
  public String
  getLastMessageReceived()
  {
    return( last_message_received==null?"":last_message_received );
  }
 
  protected List
  getYGMMarkers()
  {
    return( recent_ygm );
  }
 
  protected int
  getLastStatusSeq()
  {
    return( last_status_seq );
  }
 
  protected void
  buddyConnectionEstablished(
    boolean    outgoing )
  {
    buddyActive();
  }
 
  protected void
  buddyMessageSent(
    int      size,
    boolean    record_active )
  {
    message_out_count++;
    message_out_bytes += size;
   
    if ( record_active ){
   
      buddyActive();
    }
  }
 
  protected void
  buddyMessageReceived(
    int    size )
  {   
    message_in_count++;
    message_in_bytes += size;
   
    received_frag_details = "";
   
    buddyActive();
  }
 
  protected void
  buddyMessageFragmentReceived(
    int    num_received,
    int    total )
  {
    received_frag_details = num_received + "/" + total;
   
    plugin.fireDetailsChanged( this );
  }
 
  public String
  getMessageInFragmentDetails()
  {
    return( received_frag_details );
  }
 
  public int
  getMessageInCount()
  {
    return( message_in_count );
  }
 
  public int
  getMessageOutCount()
  {
    return( message_out_count );
  }
 
  public int
  getBytesInCount()
  {
    return( message_in_bytes );
  }
 
  public int
  getBytesOutCount()
  {
    return( message_out_bytes );
  }
 
  public boolean
  isConnected()
  {
    boolean connected = false;
   
    synchronized( this ){
           
      for (int i=0;i<connections.size();i++){
       
        buddyConnection c = (buddyConnection)connections.get(i);
       
        if ( c.isConnected() && !c.hasFailed()){
         
          connected = true;
        }
      }
    }
   
    return( connected );
  }
 
  protected void
  buddyActive()
  {
    long  now = SystemTime.getCurrentTime();
   
    synchronized( this ){
     
      last_time_online      = now;
      online            = true;
    }
     
    persistentDispatchPending();
   
    plugin.fireDetailsChanged( this );
  }
 
  public void
  ping()
    throws BuddyPluginException
  {
    plugin.checkAvailable();
   
    try{
      Map  ping_request = new HashMap();
     
      ping_request.put( "type", new Long( BuddyPlugin.RT_INTERNAL_REQUEST_PING ));
     
      sendMessage(
        BuddyPlugin.SUBSYSTEM_INTERNAL,
        ping_request,
        60*1000,
        new BuddyPluginBuddyReplyListener()
        {
          public void
          replyReceived(
            BuddyPluginBuddy  from_buddy,
            Map          reply )
          {
            log( "Ping reply received:" + reply );
          }
         
          public void
          sendFailed(
            BuddyPluginBuddy    to_buddy,
            BuddyPluginException  cause )
          {
            log( "Ping failed to " + getString(), cause );
          }
        });
     
    }catch( Throwable e ){
           
      throw( new BuddyPluginException( "Ping failed", e ));
    }
  }
 
  protected void
  sendCloseRequest(
    boolean    restarting )
  {
    List  to_send = new ArrayList();
 
    synchronized( this ){
       
      closing  = true;
     
      for (int i=0;i<connections.size();i++){
       
        buddyConnection c = (buddyConnection)connections.get(i);
       
        if ( c.isConnected() && !c.hasFailed() && !c.isActive()){
         
          to_send.add( c );
        }
      }
    }
   
    for (int i=0;i<to_send.size();i++){
     
      buddyConnection c = (buddyConnection)to_send.get(i);
     
      try{
        Map  close_request = new HashMap();
       
        close_request.put( "type", new Long( BuddyPlugin.RT_INTERNAL_REQUEST_CLOSE ));
       
        close_request.put( "r", new Long( restarting?1:0));
       
        close_request.put( "os", new Long( plugin.getCurrentStatusSeq()));
       
        final buddyMessage  message =
          new buddyMessage( BuddyPlugin.SUBSYSTEM_INTERNAL, close_request, 60*1000 );

        message.setListener(
            new BuddyPluginBuddyReplyListener()
            {
              public void
              replyReceived(
                BuddyPluginBuddy  from_buddy,
                Map          reply )
              {
                log( "Close reply received:" + reply );
              }
             
              public void
              sendFailed(
                BuddyPluginBuddy    to_buddy,
                BuddyPluginException  cause )
              {
                log( "Close failed to " + getString(), cause );
              }
            });
       
        c.sendCloseMessage( message );

      }catch( Throwable e ){
             
        log( "Close request failed", e );
      }
    }
  }
 
  protected void
  receivedCloseRequest(
    Map    request )
  {
    List<buddyConnection>  closing = new ArrayList<buddyConnection>();
   
    synchronized( this ){
     
      closing.addAll( connections );
    }
   
    for (int i=0;i<closing.size();i++){
     
      ((buddyConnection)closing.get(i)).remoteClosing();
    }
   
    try{
      boolean  restarting = ((Long)request.get( "r" )).longValue() == 1;
     
      if ( restarting ){
       
        logMessage( "restarting" );
       
      }else{
       
        logMessage( "going offline" );
       
        boolean  details_change = false;
       
        synchronized( this ){
         
          if ( offline_seq_set == null ){
           
            offline_seq_set = new HashSet<Long>();
          }
         
          offline_seq_set.add( new Long( last_status_seq ));
         
          offline_seq_set.add((Long)request.get( "os" ));
         
          if ( online ){
           
            online          = false;
            consec_connect_fails  = 0;
           
            details_change  = true;
          }
        }
       
        if ( details_change ){
         
          plugin.fireDetailsChanged( this );
        }
      }
    }catch( Throwable e ){
     
      Debug.out( "Failed to decode close request", e );
    }
  }
 
  public void
  sendMessage(
    final int                subsystem,
    final Map                content,
    final int                timeout_millis,
    final BuddyPluginBuddyReplyListener    listener )
 
    throws BuddyPluginException
  {
    plugin.checkAvailable();
   
    boolean  wait = false;
       
    if ( ip == null ){
     
      if ( check_active ){
       
        wait  = true;
       
      }else if ( SystemTime.getCurrentTime() - last_status_check_time > 30*1000 ){
       
        plugin.updateBuddyStatus( this );
       
        wait  = true;
      }
    }
   
    if ( wait ){
     
      new AEThread2( "BuddyPluginBuddy:sendWait", true )
      {
        public void
        run()
        {
          try{
            long  start = SystemTime.getCurrentTime();
           
            for (int i=0;i<20;i++){
             
              if ( ip != null ){
               
                break;
              }
             
              Thread.sleep( 1000 );
            }
           
            long  elapsed = SystemTime.getCurrentTime() - start;
           
            int new_tm = timeout_millis;
           
            if ( elapsed > 0 && timeout_millis > 0 ){
             
              new_tm -= elapsed;
             
              if ( new_tm <= 0 ){
               
                listener.sendFailed( BuddyPluginBuddy.this, new BuddyPluginException( "Timeout" ));
               
                return;
              }
            }
           
            sendMessageSupport( content, subsystem, new_tm, listener );
           
          }catch( Throwable e ){
           
            if ( e instanceof BuddyPluginException ){
             
              listener.sendFailed( BuddyPluginBuddy.this, (BuddyPluginException)e);
            }else{
           
              listener.sendFailed( BuddyPluginBuddy.this, new BuddyPluginException( "Send failed", e ));
            }
          }
        }       
      }.start();
     
    }else{
     
      sendMessageSupport( content, subsystem, timeout_millis, listener );
    }
  }
 
  protected void
  sendMessageSupport(
    final Map                content,
    final int                subsystem,
    final int                timeout_millis,
    final BuddyPluginBuddyReplyListener    original_listener )
 
    throws BuddyPluginException
  {
    boolean too_many_messages = false;
   
    synchronized( this ){
   
      too_many_messages = messages.size() >= MAX_QUEUED_MESSAGES;
    }
   
    if ( too_many_messages ){
     
      throw( new BuddyPluginException( "Too many messages queued" ));
    }
   
    final buddyMessage  message = new buddyMessage( subsystem, content, timeout_millis );
   
    BuddyPluginBuddyReplyListener  listener_delegate =
      new BuddyPluginBuddyReplyListener()
      {
        public void
        replyReceived(
          BuddyPluginBuddy    from_buddy,
          Map            reply )
        {
          // logMessage( "Msg " + message.getString() + " ok" );
         
          try{
            synchronized( BuddyPluginBuddy.this ){
             
              if ( current_message != message ){
               
                Debug.out( "Inconsistent: reply received not for current message" );
              }
             
              current_message = null;
            }
           
            original_listener.replyReceived( from_buddy, reply );
           
          }finally{
           
            dispatchMessage();
          }
        }
       
        public void
        sendFailed(
          BuddyPluginBuddy    to_buddy,
          BuddyPluginException  cause )
        {
          logMessage( "Msg " + message.getString() + " failed: " + Debug.getNestedExceptionMessage( cause ));

          try{
              // only try and reconcile this failure with the current message if
              // the message has actually been sent
           
            boolean  was_active;
           
            if ( cause instanceof BuddyPluginTimeoutException ){
             
              was_active = ((BuddyPluginTimeoutException)cause).wasActive();
             
            }else{
             
              was_active = true;
            }
           
            if ( was_active ){
             
              synchronized( BuddyPluginBuddy.this ){
               
                if ( current_message != message ){
                 
                  Debug.out( "Inconsistent: error received not for current message" );
                }
               
                current_message = null;
              }
            }
           
            long  now = SystemTime.getCurrentTime();
           
            int  retry_count = message.getRetryCount();
           
            if ( retry_count < 1 && !message.timedOut( now )){
             
              message.setRetry();
             
              // logMessage( "Msg " + message.getString() + " retrying" );

              synchronized( BuddyPluginBuddy.this ){
               
                messages.add( 0, message );
              }
            }else{
           
              original_listener.sendFailed( to_buddy, cause );
            }
          }finally{
             
            dispatchMessage();
          }
        }
      };
     
    message.setListener( listener_delegate );
     
    int  size;
   
    synchronized( this ){
     
      messages.add( message );
     
      size = messages.size();
    }
   
    // logMessage( "Msg " + message.getString() + " added: num=" + size );
   
    dispatchMessage();
  }
 
  protected void
  dispatchMessage()
  {
    buddyConnection  bc = null;
   
    buddyMessage   allocated_message   = null;
    Throwable    failed_msg_error   = null;
   
    boolean  inform_dirty  = false;
   
    synchronized( this ){
 
      if ( current_message != null || messages.size() == 0 || closing ){
       
        return;
      }
     
      allocated_message = current_message = (buddyMessage)messages.remove( 0 );
     
      for (int i=0;i<connections.size();i++){
       
        buddyConnection c = (buddyConnection)connections.get(i);
       
        if ( !c.hasFailed()){
         
          bc  = c;
        }
      }
     
      if ( bc == null ){
       
        if ( destroyed ){
         
          failed_msg_error = new BuddyPluginException( "Friend destroyed" );
         
        }else if ( connections.size() >= MAX_ACTIVE_CONNECTIONS ){
         
          failed_msg_error = new BuddyPluginException( "Too many active connections" );
        }
      }
    }
   
    if ( failed_msg_error != null ){
     
      allocated_message.reportFailed( failed_msg_error );
     
      return;
    }
   
    if ( bc == null ){
     
        // single-thread outgoing connect attempts
     
      try{
        outgoing_connect_sem.reserve();
     
        synchronized( this ){

          if ( current_message != allocated_message ){
           
            failed_msg_error = new BuddyPluginException( "current message no longer active" );
           
          }else if ( closing ){
           
            return;
          }
           
          if ( failed_msg_error == null ){
           
            for (int i=0;i<connections.size();i++){
             
              buddyConnection c = (buddyConnection)connections.get(i);
             
              if ( !c.hasFailed()){
               
                bc  = c;
              }
            }
           
            if ( bc == null ){
             
              if ( destroyed ){
               
                failed_msg_error = new BuddyPluginException( "Friend destroyed" );
               
              }else if ( connections.size() >= MAX_ACTIVE_CONNECTIONS ){
               
                failed_msg_error = new BuddyPluginException( "Too many active connections" );
              }
            }
          }
        }
       
        if ( bc == null && failed_msg_error == null ){
         
          try{
              // can't perform connect op while synchronized as may deadlock on password
              // aquisition
           
            GenericMessageConnection generic_connection = outgoingConnection();

            synchronized( this ){

              if ( current_message != allocated_message ){
               
                failed_msg_error = new BuddyPluginException( "current message no longer active" );
               
                generic_connection.close();
               
              }else{
               
                bc = new buddyConnection( generic_connection, true );
               
                inform_dirty = connections.size() == 0;
               
                connections.add( bc );
             
                // logMessage( "Con " + bc.getString() + " added: num=" + connections.size() );
              }
            }
          }catch( Throwable e ){
           
            failed_msg_error = e;
          }
        }
      }finally{
       
        outgoing_connect_sem.release();
      }
    }
     
    if ( failed_msg_error != null ){
     
      allocated_message.reportFailed( failed_msg_error );
     
      return;
    }
     
    try{
      // logMessage( "Allocating msg " + allocated_message.getString() + " to con " + bc.getString());

      bc.sendMessage( allocated_message );
   
    }catch( BuddyPluginException e ){
     
      allocated_message.reportFailed( e );
    }
   
    if ( inform_dirty ){
     
      plugin.setConfigDirty();
    }
  }
 
  protected void
  removeConnection(
    buddyConnection      bc )
  {
    int  size;
   
    synchronized( this ){
     
      connections.remove( bc );
     
      size = connections.size();
    }
   
    if ( size == 0 ){
             
      plugin.setConfigDirty();
    }
   
    if ( size == 0 && bc.isConnected() && !bc.isClosing() && !bc.isRemoteClosing()){
     
        // dropped connection, kick in a keep alive
     
      if ( consec_connect_fails < 3 ){
       
        if ( consec_connect_fails == 0 ){
         
          long  now = SystemTime.getMonotonousTime();
         
          boolean do_it = false;
         
          synchronized( this ){
         
            if (   last_auto_reconnect == -1 ||
                now - last_auto_reconnect > 30*1000 ){
             
              last_auto_reconnect = now;
             
              do_it = true;
            }
          }
         
          if ( do_it ){
           
              // delay a bit
           
            new DelayedEvent(
                "BuddyPluginBuddy:recon",
                new Random().nextInt( 3000 ),
                new AERunnable()
                {
                  public void
                  runSupport()
                  {
                    int  size;
                   
                    synchronized( BuddyPluginBuddy.this ){
                                         
                      size = connections.size();
                    }
                   
                    if ( consec_connect_fails == 0 && size == 0 ){
       
                      log( "Attempting reconnect after dropped connection" );
                     
                      sendKeepAlive();
                    }
                  }
                });
          }
             
        }else{
         
          long  delay = 60*1000;
         
          delay <<= Math.min( 3, consec_connect_fails );
     
          if ( SystemTime.getCurrentTime() - last_connect_attempt >= delay ){
           
            sendKeepAlive();
          }
        }
      }
    }
   
    // logMessage( "Con " + bc.getString() + " removed: num=" + size );

      // connection failed, see if we need to attempt to re-establish
   
    plugin.fireDetailsChanged( this );
   
    dispatchMessage();
  }
 
  protected long
  getLastStatusCheckTime()
  {
    return( last_status_check_time );
  }
 
  protected boolean
  statusCheckActive()
  {
    synchronized( this ){

      return( check_active );
    }
  }
 
  protected boolean
  statusCheckStarts()
  {
    synchronized( this ){
     
      if ( check_active ){
       
        return( false );
      }
   
      last_status_check_time = SystemTime.getCurrentTime();
   
      check_active = true;
    }
   
    return( true );
  }
 
  protected void
  statusCheckFailed()
  {
    boolean  details_change = false;

    synchronized( this ){

      try{
        if ( online ){
         
          online          = false;
          consec_connect_fails  = 0;
         
          details_change  = true;
        }
      }finally{
     
        status_check_count++;
       
        check_active = false;
      }
    }
     
    if ( details_change ){
     
      plugin.fireDetailsChanged( this );
    }
  }
 
  protected void
  setCachedStatus(
    InetAddress    _ip,
    int        _tcp_port,
    int        _udp_port )
  {
    synchronized( this ){

      if ( ip == null ){
     
        ip      = _ip;
        tcp_port  = _tcp_port;
        udp_port  = _udp_port;
      }
    }
  }
 
  protected void
  statusCheckComplete(
    long      _post_time,
    InetAddress    _ip,
    int        _tcp_port,
    int        _udp_port,
    String      _nick_name,
    int        _online_status,
    int        _status_seq,
    int        _version )
  {
    boolean  details_change   = false;
    boolean  config_dirty   = false;

    long  now = SystemTime.getCurrentTime();
   
    if ( now < last_time_online ){
     
      last_time_online = now;
    }
   
    boolean is_connected = isConnected();
   
    synchronized( this ){

      try{
          // do we explicitly know that this sequence number denotes an offline buddy
       
        if ( offline_seq_set != null ){
         
          if ( offline_seq_set.contains(new Long( _status_seq ))){
           
            return;
           
          }else{
           
            offline_seq_set = null;
          }
        }
       
        boolean  seq_change = _status_seq != last_status_seq;
       
        boolean timed_out;

          // change in sequence means we're online
       
        if ( seq_change ){
         
          last_status_seq    = _status_seq;
          last_time_online  = now;
         
          timed_out       = false;
          details_change    = true;
         
        }else{
       
          timed_out =  now - last_time_online >= BuddyPlugin.STATUS_REPUBLISH_PERIOD * 3 ;
       
       
        if ( online ){
         
          if ( timed_out ){
             
            online          = false;
            consec_connect_fails  = 0;
           
            details_change  = true;
          }
        }else
         
          if ( seq_change || !timed_out ){
           
            online      = true;
            details_change  = true;
          }
        }
       
        post_time  = _post_time;
       
        if (   !addressesEqual( ip, _ip ) ||
            tcp_port != _tcp_port ||
            udp_port != _udp_port ||
            version   < _version ){
         
          ip        = _ip;
          tcp_port    = _tcp_port;
          udp_port    = _udp_port;
         
          if ( version < _version ){
         
            version      = _version;
          }
         
          details_change  = true;
        }
       
          // if we are connected then we use the status sent over the connection
          // as it is more up to date
       
        if (   !is_connected &&
            online_status != _online_status ){
         
          online_status  = _online_status;
         
          details_change  = true;
        }
       
        if ( !plugin.stringsEqual( nick_name, _nick_name )){
         
          nick_name  = _nick_name;
         
          config_dirty  = true;
          details_change  = true;
        }
      }finally{
     
        status_check_count++;
         
        check_active = false;
      }
    }
   
    if ( config_dirty ){
     
      plugin.setConfigDirty();
    }
   
    if ( details_change ){
     
      if ( online ){
       
        persistentDispatchPending();
      }
     
      plugin.fireDetailsChanged( this );
    }
   
    plugin.logMessage( getString());
  }
 
  protected boolean
  addressesEqual(
    InetAddress    ip1,
    InetAddress    ip2 )
  {
    if ( ip1 == null && ip2 == null ){
     
      return( true );
     
    }else if ( ip1 == null || ip2 == null ){
     
      return( false );
     
    }else{
 
      return( ip1.equals( ip2 ));
    }
  }
 
  protected void
  checkTimeouts()
  {
    long  now = SystemTime.getCurrentTime();
   
    List  failed = null;
   
    List  connections_to_check = null;
   
    boolean  messages_queued;
   
    synchronized( this ){
     
      messages_queued = messages.size() > 0;
     
      if ( messages_queued ){
             
        Iterator  it = messages.iterator();
       
        while( it.hasNext()){
         
          buddyMessage  message = (buddyMessage)it.next();
         
          if ( message.timedOut( now )){
           
            it.remove();
           
            if ( failed == null ){
             
              failed = new ArrayList();
            }
           
            failed.add( message );
          }
        }
      }
     
      if ( connections.size() > 0 ){
       
        connections_to_check = new ArrayList( connections );
      }
    }
   
    boolean  send_keep_alive = false;

    if ( connections_to_check == null ){     
     
        // no active connections
     
      if ( online && ip != null && !messages_queued ){
       
          // see if we should attempt a pre-emptive connect
       
        if ( consec_connect_fails < 3 ){
         
          long  delay = 60*1000;
                     
          delay <<= Math.min( 3, consec_connect_fails );
       
          send_keep_alive = now - last_connect_attempt >= delay;
        }
      }
    }else{
     
      for (int i=0;i<connections_to_check.size();i++){
       
        buddyConnection connection = (buddyConnection)connections_to_check.get(i);
       
        boolean closed = connection.checkTimeout( now );
       
        if (   ip != null &&
            !closed &&
            !messages_queued &&
            connection.isConnected() &&
            !connection.isActive()){
         
          if ( now - connection.getLastActive( now ) > CONNECTION_KEEP_ALIVE ){
                 
            send_keep_alive  = true;
          }
        }
      }
    }
   
    if ( send_keep_alive ){
     
      sendKeepAlive();
    }
   
    if ( failed != null ){
     
      for (int i=0;i<failed.size();i++){
       
        ((buddyMessage)failed.get(i)).reportFailed( new BuddyPluginTimeoutException( "Timeout", false ));
      }
    }
  }
 
  protected void
  sendKeepAlive()
  {
    boolean send_keep_alive = true;
   
    synchronized( this ){
     
      if ( keep_alive_outstanding ){
       
        send_keep_alive = false;
       
      }else{
     
        keep_alive_outstanding = true;
      }
    }
   
    if ( send_keep_alive ){
     
      try{
        Map  ping_request = new HashMap();
       
        ping_request.put( "type", new Long( BuddyPlugin.RT_INTERNAL_REQUEST_PING ));
       
        sendMessageSupport(
          ping_request,
          BuddyPlugin.SUBSYSTEM_INTERNAL,
          60*1000,
          new BuddyPluginBuddyReplyListener()
          {
            public void
            replyReceived(
              BuddyPluginBuddy  from_buddy,
              Map          reply )
            {
              synchronized( BuddyPluginBuddy.this ){
               
                keep_alive_outstanding = false;
              }
            }
           
            public void
            sendFailed(
              BuddyPluginBuddy    to_buddy,
              BuddyPluginException  cause )
            {
              synchronized( BuddyPluginBuddy.this ){
               
                keep_alive_outstanding = false;
              }
            }
          });
       
      }catch( Throwable e ){
       
        synchronized( this ){
         
          keep_alive_outstanding = false;
        }
      }
    }
  }
 
  public String
  getConnectionsString()
  {
    synchronized( this ){
     
      String  str = "";
     
      for (int i=0;i<connections.size();i++){
       
        str += (str.length()==0?"":",") + ((buddyConnection)connections.get(i)).getString( true );
      }
     
      return( str );
    }
  }
 
  public void
  disconnect()
  {
    List  to_disconnect = new ArrayList();
   
    synchronized( this ){
     
      to_disconnect.addAll( connections );
    }
   
    for (int i=0;i<to_disconnect.size();i++){
     
      ((buddyConnection)to_disconnect.get(i)).disconnect();
    }
  }
 
  protected boolean
  isClosing()
  {
    return( closing );
  }
 
  protected void
  destroy()
  {
    List<buddyConnection>  to_close = new ArrayList<buddyConnection>();
   
    synchronized( this ){
     
      destroyed = true;
     
      to_close.addAll( connections );
    }
   
    for (int i=0;i<to_close.size();i++){
     
      ((buddyConnection)to_close.get(i)).close();
    }
  }
 
  protected void
  logMessage(
    String  str )
  {
    plugin.logMessage( getShortString() + ": " + str );
  }
 
  protected GenericMessageConnection
  outgoingConnection()
 
    throws BuddyPluginException
  {
    GenericMessageRegistration msg_registration = plugin.getMessageRegistration();

    if ( msg_registration == null ){
           
      throw( new BuddyPluginException( "Messaging system unavailable" ));
    }
   
    InetAddress ip = getIP();
   
    if ( ip == null ){
           
      throw( new BuddyPluginException( "Friend offline (no usable IP address)" ));
    }
   
    InetSocketAddress  tcp_target  = null;
    InetSocketAddress  udp_target  = null;
   
    int  tcp_port = getTCPPort();
   
    if ( tcp_port > 0 ){
     
      tcp_target = new InetSocketAddress( ip, tcp_port );
    }
   
    int  udp_port = getUDPPort();
   
    if ( udp_port > 0 ){
     
      udp_target = new InetSocketAddress( ip, udp_port );
    }

    InetSocketAddress  notional_target = tcp_target;
   
    if ( notional_target == null ){
   
      notional_target = udp_target;
    }
   
    if ( notional_target == null ){
           
      throw( new BuddyPluginException( "Friend offline (no usable protocols)" ));
    }
   
    GenericMessageEndpoint  endpoint = msg_registration.createEndpoint( notional_target );
   
    if ( tcp_target != null ){
   
      endpoint.addTCP( tcp_target );
    }
   
    if ( udp_target != null ){
   
      endpoint.addUDP( udp_target );
    }
       
    GenericMessageConnection  con = null;
   
    try{
      last_connect_attempt = SystemTime.getCurrentTime();
     
      con = msg_registration.createConnection( endpoint );
       
      plugin.addRateLimiters( con );
     
      String reason = "Friend: Outgoing connection establishment";
 
      SESecurityManager sec_man = plugin.getSecurityManager();
     
      con = sec_man.getSTSConnection(
          con,
          sec_man.getPublicKey( SEPublicKey.KEY_TYPE_ECC_192, reason ),
 
          new SEPublicKeyLocator()
          {
            public boolean
            accept(
              Object      context,
              SEPublicKey    other_key )
            {
              String  other_key_str = Base32.encode( other_key.encodeRawPublicKey());

              if ( other_key_str.equals( public_key )){
               
                consec_connect_fails  = 0;
               
                return( true );
               
              }else{
               
                log( getString() + ": connection failed due to pk mismatch" );
             
                return( false );
              }
            }
          },
          reason,
          BuddyPlugin.BLOCK_CRYPTO );   
       
      con.connect();
     
      return( con );
     
    }catch( Throwable e ){
     
      if ( con != null ){
     
        consec_connect_fails++;
       
        try{
          con.close();
         
        }catch( Throwable f ){
         
          log( "Failed to close connection", f );
        }
      }
     
      throw( new BuddyPluginException( "Failed to send message", e ));
    }
  }
 
  protected void
  incomingConnection(
    GenericMessageConnection  _connection )
 
    throws BuddyPluginException
  {
    addConnection( _connection );
  }
 
  protected void
  addConnection(
    GenericMessageConnection    _connection )
 
    throws BuddyPluginException
  {
    //int  size;
   
    buddyConnection bc = new buddyConnection( _connection, false );
   
    boolean inform_dirty = false;
   
    synchronized( this ){
     
      if ( destroyed ){
       
        throw( new BuddyPluginException( "Friend has been destroyed" ));
      }
     
      inform_dirty = connections.size() == 0;
     
      connections.add( bc );
     
      //size = connections.size();
    }
   
    // logMessage( "Con " + bc.getString() + " added: num=" + size );
   
    if ( inform_dirty ){
     
      plugin.setConfigDirty();
    }
  }
 
  public void
  setUserData(
    Object    key,
    Object    value )
  {
    synchronized( user_data ){
     
      user_data.put(key, value);
    }
  }
 
  public Object
  getUserData(
    Object    key )
  {
    synchronized( user_data ){

      return( user_data.get( key ));
    }
  }
 
  protected void
  log(
    String    str )
  {
    plugin.log( str );
  }
 
  protected void
  log(
    String    str,
    Throwable   e )
  {
    plugin.log( str, e );
  }
 
  public String
  getString()
  {
    return( "pk=" +  getShortString() + (nick_name==null?"":(",nick=" + nick_name)) + ",ip=" + ip + ",tcp=" + tcp_port + ",udp=" + udp_port + ",online=" + online + ",age=" + (SystemTime.getCurrentTime() - post_time ));
  }

  protected class
  buddyMessage
  {
    private int                  message_id;
   
    private Map                  request;
    private int                  subsystem;
    private BuddyPluginBuddyReplyListener    listener;
    private int                  timeout_millis;
   
    private long          queue_time  = SystemTime.getCurrentTime();
   
    private boolean    timed_out;
    private int      retry_count;
    private boolean    complete;
   
    protected
    buddyMessage(
      int                  _subsystem,
      Map                  _request,
      int                  _timeout )
    {
      synchronized( BuddyPluginBuddy.this ){
     
        message_id = next_message_id++;
      }
     
      request      = _request;
      subsystem    = _subsystem;
      timeout_millis  = _timeout;
    }
   
    protected void
    setListener(
      BuddyPluginBuddyReplyListener    _listener )
    {
      listener    = _listener;
    }
   
    protected int
    getRetryCount()
    {
      synchronized( this ){

        return( retry_count );
      }
    }
   
    protected void
    setDontRetry()
    {
      retry_count = 99;
    }
   
    protected void
    setRetry()
    {
      synchronized( this ){
       
        retry_count++;
       
        complete   = false;
        timed_out   = false;
       
      }
    }
   
    protected boolean
    timedOut(
      long  now )
    {
      if ( timed_out ){
       
        return( true );
      }
     
      if ( now < queue_time ){
       
        queue_time = now;
       
        return( false );
       
      }else{
       
        timed_out = now - queue_time >= timeout_millis;
       
        return( timed_out );
      }
    }
   
    protected Map
    getRequest()
    {
      return( request );
    }
   
    protected int
    getSubsystem()
    {
      return( subsystem );
    }
   
    protected int
    getID()
    {
      return( message_id );
    }
   
    protected void
    reportComplete(
      Map    reply )
    {
      synchronized( this ){
       
        if ( complete ){
         
          return;
        }
       
        complete = true;
      }
     
      try{
        listener.replyReceivedBuddyPluginBuddy.this, reply );
       
      }catch( Throwable e ){
       
        Debug.printStackTrace(e);
      }
    }
   
    protected void
    reportFailed(
      Throwable  error )
    {
      synchronized( this ){
       
        if ( complete ){
         
          return;
        }
       
        complete = true;
      }
     
      try{
        if ( error instanceof BuddyPluginException ){
         
          listener.sendFailed( BuddyPluginBuddy.this, (BuddyPluginException)error );
         
        }else{
       
          listener.sendFailedBuddyPluginBuddy.this, new BuddyPluginException( "",  error ));
        }
      }catch( Throwable e ){
       
        Debug.printStackTrace(e);
      }
    }
   
    protected String
    getString()
    {
      return( "id=" + message_id + ",ss=" + subsystem + (retry_count==0?"":(",retry="+retry_count)));
    }
  }
 
  protected class
  buddyConnection
    implements fragmentHandlerReceiver
  {
    private fragmentHandler          fragment_handler;
    private int                connection_id;
    private boolean              outgoing;
   
    private String              dir_str;
   
   
    private volatile buddyMessage  active_message;
   
    private volatile boolean      connected;
    private volatile boolean      closing;
    private volatile boolean      remote_closing;
    private volatile boolean      failed;
   
    private long      last_active  = SystemTime.getCurrentTime();
   
    protected
    buddyConnection(
      GenericMessageConnection    _connection,
      boolean              _outgoing )
    {
      fragment_handler  = new fragmentHandler( _connection, this );
     
      outgoing  = _outgoing;
     
      synchronized( BuddyPluginBuddy.this ){
       
        connection_id = next_connection_id++;
      }
     
      dir_str = outgoing?"Outgoing":"Incoming";
     
      if ( !outgoing ){
       
        connected = true;
       
        buddyConnectionEstablished( false );
      }
     
      fragment_handler.start();
    }
   
    protected boolean
    isConnected()
    {
      return( connected );
    }
   
    protected boolean
    hasFailed()
    {
      return( failed );
    }
   
    protected boolean
    isOutgoing()
    {
      return( outgoing );
    }
   
    protected long
    getLastActive(
      long    now )
    {
      if ( now < last_active ){
       
        last_active = now;
      }
     
      return( last_active );
    }
   
    protected void
    sendMessage(
      buddyMessage  message )
   
      throws BuddyPluginException
    {     
      BuddyPluginException  failed_error = null;

      buddyMessage  msg_to_send      = null;

      synchronized( this ){
       
        if ( BuddyPluginBuddy.this.isClosing()){
         
          throw( new BuddyPluginException( "Close in progress" ));
        }
       
        if ( active_message != null ){
         
          Debug.out( "Inconsistent: active message already set" );
         
          failed_error = new BuddyPluginException( "Inconsistent state" );
         
        }else if ( failed || closing ){
         
          throw( new BuddyPluginException( "Connection failed" ));
         
        }else{
                   
          active_message = message;
         
          if ( connected ){
           
            msg_to_send = active_message;
          }
        }
      }
     
      if ( failed_error != null ){
       
        failed( failed_error );
       
        throw( failed_error );
      }
     
      if ( msg_to_send != null ){
     
        send( msg_to_send );
      }
    }
   
    protected void
    sendCloseMessage(
      buddyMessage  message
    {     
      boolean  ok_to_send;
     
      synchronized( this ){
       
        ok_to_send = active_message == null && connected && !failed && !closing;
      }
     
      if ( ok_to_send ){
     
        send( message );
      }
    }
   
    public boolean
    isActive()
    {
      return( active_message != null );
    }
   
    public void
    connected()
    {
      if ( TRACE ){
        System.out.println( dir_str + " connected" );
      }
     
      buddyMessage  msg_to_send = null;
     
      synchronized( this ){
       
        last_active  = SystemTime.getCurrentTime();
       
        connected = true;
       
        msg_to_send = active_message;
      }
     
      buddyConnectionEstablished( true );
     
      if ( msg_to_send != null  ){
     
        send( msg_to_send );
      }
    }
   
    protected boolean
    checkTimeout(
      long  now )
    {
      buddyMessage  bm = null;
     
      boolean  close = false;
     
      synchronized( this ){

        if ( active_message != null ){
         
          if ( active_message.timedOut( now )){
           
            bm  = active_message;
           
            active_message  = null;
          }
        }
               
        if ( now < last_active ){
         
          last_active = now;
        }
       
        if ( now - last_active > CONNECTION_IDLE_TIMEOUT ){
                   
          close  = true;
        }
      }
     
      if ( bm != null ){
       
        bm.reportFailed( new BuddyPluginTimeoutException( "Timeout", true ));
      }

      if ( close ){
       
        close();
      }
     
      return( close );
    }
   
    protected void
    send(
      buddyMessage    msg )
    {
      Map request = msg.getRequest();
     
      Map  send_map = new HashMap();
     
      send_map.put( "type", new Long( RT_REQUEST_DATA ));
      send_map.put( "req", request );
      send_map.put( "ss", new Long( msg.getSubsystem()));
      send_map.put( "id", new Long( msg.getID()));
      send_map.put( "oz", new Long( plugin.getOnlineStatus()));
      send_map.put( "v", new Long( BuddyPlugin.VERSION_CURRENT ));
     
      String  loc_cat = getLocalAuthorisedRSSCategoriesAsString();
     
      if ( loc_cat != null ){
        send_map.put( "cat", loc_cat );
      }
     
      try{
        // logMessage( "Sending " + msg.getString() + " to " + getString());

        fragment_handler.send( send_map, true, true );
             
        synchronized( this ){
         
          last_active  = SystemTime.getCurrentTime();;
        }
      }catch( BuddyPluginException e ){
     
        try{     
          failed( e );
       
        }catch( Throwable f ){
       
          Debug.printStackTrace(f);
        }
      }
    }
   
    public void
    receive(
      Map      data_map )
    {
      synchronized( this ){
       
        last_active  = SystemTime.getCurrentTime();;
      }
     
      if ( TRACE ){
        System.out.println( dir_str + " receive: " + data_map );
      }

      try{
        int  type = ((Long)data_map.get("type")).intValue();
       
        Long  l_os = (Long)data_map.get( "oz" );
       
        if ( l_os != null ){
         
          setOnlineStatus( l_os.intValue());
        }
       
        Long  l_ver = (Long)data_map.get( "v" );
       
        if ( l_ver != null ){
         
          setVersion( l_ver.intValue());
        }
       
        byte[]  b_rem_cat = (byte[])data_map.get( "cat" );
       
        if ( b_rem_cat == null ){
         
          setRemoteAuthorisedRSSCategories( null );
         
        }else{
         
          setRemoteAuthorisedRSSCategories( stringToCats( new String( b_rem_cat, "UTF-8" )));
        }
       
        if ( type == RT_REQUEST_DATA ){
         
          // logMessage( "Received type=" + type + " from " + getString());
       
          Long  subsystem = (Long)data_map.get( "ss" );
         
          Map  reply;
         
          int  reply_type;
         
          Map request = (Map)data_map.get( "req" );

          String  error = null;
         
          if ( request == null || subsystem == null ){
           
            reply  = null;
           
          }else{
           
            try{
           
              reply = plugin.requestReceived( BuddyPluginBuddy.this, subsystem.intValue(), request );
             
            }catch( Throwable e ){
             
              error = Debug.getNestedExceptionMessage(e);
             
              reply = null;
            }
          }
         
          if ( reply == null ){
           
            reply_type = RT_REPLY_ERROR;
           
            reply = new HashMap();
           
            reply.put( "error", error==null?"No handlers available to process request":error );
           
          }else{
           
            reply_type = RT_REPLY_DATA;
          }
         
          Map reply_map = new HashMap();
         
          reply_map.put( "ss", subsystem );
          reply_map.put( "type", new Long( reply_type ));                               
          reply_map.put( "id", data_map.get( "id" ) );
          reply_map.put( "oz", new Long( plugin.getOnlineStatus()));

          String  loc_cat = getLocalAuthorisedRSSCategoriesAsString();
         
          if ( loc_cat != null ){
            reply_map.put( "cat", loc_cat );
          }
         
          reply_map.put( "rep", reply );
         
            // don't record as active here as (1) we recorded as active above when
            // receiving request (2) we may be replying to a 'closing' message and
            // we don't want the reply to mark as online

          fragment_handler.send( reply_map, false, false );
         
        }else if ( type == RT_REPLY_DATA || type == RT_REPLY_ERROR ){
         
          long  id = ((Long)data_map.get( "id" )).longValue();
         
          buddyMessage  bm;
         
          synchronized( this ){
           
            if (   active_message != null &&
                active_message.getID() == id ){
               
              bm = active_message;
   
              active_message = null;
             
            }else{
               
              bm = null;
            }
          }
         
          Map  reply = (Map)data_map.get( "rep" );
         
          if ( bm == null ){
           
            logMessage( "reply discarded as no matching request: " + reply );
           
          }else{
           
            if ( type == RT_REPLY_ERROR ){
             
              bm.setDontRetry();
             
              bm.reportFailed( new BuddyPluginException(new String((byte[])reply.get( "error" ))));
             
            }else{
             
              bm.reportComplete( reply );
            }
          }       
        }else{
         
            // ignore unknown message types
        }
      }catch( Throwable e ){
       
        failed( e )
      }
    }
   
    protected void
    close()
    {
      closing = true;
     
      failed( new BuddyPluginException( "Closing" ));
    }
   
    protected boolean
    isClosing()
    {
      return( closing );
    }
   
    protected void
    remoteClosing()
    {
      remote_closing = true;
    }
   
    protected boolean
    isRemoteClosing()
    {
      return( remote_closing );
    }
   
    protected void
    disconnect()
    {
      fragment_handler.close();
    }
   
    public void
    failed(
      Throwable           error )
    {
      buddyMessage bm = null;
     
      if ( !connected && outgoing ){
       
        consec_connect_fails++;
      }
     
      synchronized( this ){
       
        if ( failed ){
         
          return;
        }
       
        failed = true;
       
        bm = active_message;
       
        active_message   = null;
      }
     
      logMessage( "Con " + getString() + " failed: " + Debug.getNestedExceptionMessage( error ));
     
      try{
        if ( !closing ){
         
          if ( TRACE ){
            System.out.println( dir_str + " connection error:" );
          }
        }
       
        fragment_handler.close();

      }finally{
               
        removeConnection( this );
       
        if ( bm != null ){
         
          bm.reportFailed( error );
        }
      }
    }
   
    protected String
    getString()
    {
      return( getString( false ));
    }
   
    protected String
    getString(
      boolean  short_form )
    {
      if ( short_form ){
       
        return( fragment_handler.getString());
       
      }else{
       
        return("id=" + connection_id + ",dir=" + ( outgoing?"out":"in" ));
      }
    }
  }
 
  protected class
  fragmentHandler
    implements GenericMessageConnectionListener
  {
    private GenericMessageConnection  connection;
    private fragmentHandlerReceiver    receiver;
   
    private int  next_fragment_id  = 0;
   
    private fragmentAssembly  current_request_frag;
    private fragmentAssembly  current_reply_frag;
   
    private int          send_count;
    private int          recv_count;
   
    protected
    fragmentHandler(
      GenericMessageConnection  _connection,
      fragmentHandlerReceiver    _receiver )
    {
      connection  = _connection;
      receiver  = _receiver; 
    }
 
    public void
    start()
    {
      connection.addListener( this );
    }
   
    public void
    connected(
      GenericMessageConnection  connection )
    {
      receiver.connected();
    }
   
    public void
    failed(
      GenericMessageConnection  connection,
      Throwable           error )
   
      throws MessageException
    {
      receiver.failed( error );
    }
   
    protected void
    send(
      Map      data_map,
      boolean    is_request,
      boolean    record_active )
     
      throws BuddyPluginException
    {
      try{
        byte[] data = BEncoder.encode( data_map );
       
        int  data_length = data.length;
       
        plugin.checkMaxMessageSize( data_length );
       
        int  max_chunk = connection.getMaximumMessageSize() - 1024;
               
        if ( data_length > max_chunk ){
         
          int  fragment_id;
         
          synchronized( this ){
           
            fragment_id = next_fragment_id++;
          }
         
          int chunk_num = 0;
         
          for (int i=0;i<data_length;i+=max_chunk){
           
            int  end = Math.min( data_length, i + max_chunk );
           
            if ( end > i ){
             
              byte[]  chunk = new byte[ end-i ];
             
              System.arraycopy( data, i, chunk, 0, chunk.length );
             
              Map  chunk_map = new HashMap();
             
              chunk_map.put( "type", new Long( BuddyPlugin.RT_INTERNAL_FRAGMENT ));
              chunk_map.put( "f", new Long( fragment_id ));
              chunk_map.put( "l", new Long( data_length ));
              chunk_map.put( "c", new Long( max_chunk ));
              chunk_map.put( "i", new Long( chunk_num ));
              chunk_map.put( "q", new Long( is_request?1:0 ));
              chunk_map.put( "d", chunk );
             
              byte[] chunk_data = BEncoder.encode( chunk_map );

              PooledByteBuffer chunk_buffer =
                plugin.getPluginInterface().getUtilities().allocatePooledByteBuffer( chunk_data );
           
              try{                 
                connection.send( chunk_buffer );
             
                chunk_buffer = null;
               
              }finally{
               
                if ( chunk_buffer != null ){
                 
                  chunk_buffer.returnToPool();
                }
              }
            }
           
            chunk_num++;
          }
        }else{
         
          PooledByteBuffer buffer =
            plugin.getPluginInterface().getUtilities().allocatePooledByteBuffer( data );
       
          try{     
         
            connection.send( buffer );
         
            buffer = null;
           
          }finally{
           
            if ( buffer != null ){
             
              buffer.returnToPool();
            }
          }
        }
       
        buddyMessageSent( data.length, record_active );
       
        send_count++;
       
      }catch( Throwable e ){
       
        throw( new BuddyPluginException( "Send failed", e ));
      }
    }
   
    public void
    receive(
      GenericMessageConnection  connection,
      PooledByteBuffer      message )
   
      throws MessageException
    {
      try{
          // while in unauth state we only allow a few messages. max should be 1
          // for an 'accept request' but I feel generous
       
        if ( recv_count >= 4 && !isAuthorised()){
       
          throw( new MessageException( "Too many messages received while in unauthorised state" ));
        }
       
        byte[]  content = message.toByteArray();
       
        Map  data_map = BDecoder.decode( content );
       
        if (((Long)data_map.get( "type" )).intValue() == BuddyPlugin.RT_INTERNAL_FRAGMENT ){
         
          Map  chunk_map = data_map;
         
          int  fragment_id = ((Long)chunk_map.get( "f" )).intValue();
          int  data_length = ((Long)chunk_map.get( "l" )).intValue();
          int  chunk_size   = ((Long)chunk_map.get( "c" )).intValue();
          int  chunk_num   = ((Long)chunk_map.get( "i" )).intValue();
         
          boolean  is_request = ((Long)chunk_map.get("q")).intValue() == 1;
         
          byte[]  chunk_data = (byte[])chunk_map.get("d" );
         
          plugin.checkMaxMessageSize( data_length );
         
          fragmentAssembly assembly;
         
          if ( is_request ){
           
            if ( current_request_frag == null ){
             
              current_request_frag = new fragmentAssembly( fragment_id, data_length, chunk_size );
            }
           
            assembly = current_request_frag;
           
          }else{
           
            if ( current_reply_frag == null ){
             
              current_reply_frag = new fragmentAssembly( fragment_id, data_length, chunk_size );
            }
           
            assembly = current_reply_frag;
          }
         
          if ( assembly.getID() != fragment_id ){
             
            throw( new BuddyPluginException( "Fragment receive error: concurrent decode not supported" ));
          }
         
          if ( assembly.receive( chunk_num, chunk_data )){
                   
            if ( is_request ){

              current_request_frag = null;
             
            }else{
             
              current_reply_frag = null;
            }
           
            buddyMessageReceived( data_length );

            recv_count++;
           
            receiver.receive( BDecoder.decode( assembly.getData()));
           
          }else{
           
            buddyMessageFragmentReceived( assembly.getChunksReceived(), assembly.getTotalChunks());
          }
        }else{
       
          buddyMessageReceived( content.length );

          recv_count++;
         
          receiver.receive( data_map );         
        }
      }catch( Throwable e ){
       
        receiver.failed( e );
       
      }finally{
       
        message.returnToPool();
      }
    }
   
    protected void
    close()
    {
      try{
     
        connection.close();
       
      }catch( Throwable e ){
       
        // Debug.printStackTrace( e );
       
      }finally{
       
        receiver.failed( new Exception( "Connection closed" ));
      }
    }
   
    protected String
    getString()
    {
      return( connection.getType());
    }
   
    protected class
    fragmentAssembly
    {
      private int    id;
      private byte[]  data;
      private int    chunk_size;
     
      private int    num_chunks;
      private Set    chunks_received = new HashSet();
     
      protected
      fragmentAssembly(
        int    _id,
        int    _length,
        int    _chunk_size )
      {
        id      = _id;
        chunk_size  = _chunk_size;
       
        data    = new byte[_length];
       
        num_chunks = (_length + chunk_size - 1 )/chunk_size;
      }
     
      protected int
      getID()
      {
        return( id );
      }
     
      protected int
      getChunksReceived()
      {
        return( chunks_received.size());
      }
     
      protected int
      getTotalChunks()
      {
        return( num_chunks );
      }
     
      protected boolean
      receive(
        int    chunk_num,
        byte[]  chunk )
      {
        // System.out.println( "received chunk " + chunk_num + " of " + num_chunks );
       
        Integer  i = new Integer( chunk_num );
       
        if ( chunks_received.contains( i )){
         
          return( false );
        }
       
        chunks_received.add( i );
       
        System.arraycopy( chunk, 0, data, chunk_num*chunk_size, chunk.length );
       
        return( chunks_received.size() == num_chunks );
      }
     
      protected byte[]
      getData()
      {
        return( data );
      }
    }
  } 
 
  interface
  fragmentHandlerReceiver
  {
    public void
    connected();
   
    public void
    receive(
      Map      data );
   
    public void
    failed(
      Throwable  error );
  }
}
TOP

Related Classes of com.aelitis.azureus.plugins.net.buddy.BuddyPluginBuddy$buddyMessage

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.