/*
* 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.replyReceived( BuddyPluginBuddy.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.sendFailed( BuddyPluginBuddy.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 );
}
}