All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jadex.base.relay.RelayHandler Maven / Gradle / Ivy

package jadex.base.relay;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import jadex.bridge.BasicComponentIdentifier;
import jadex.bridge.fipa.SFipa;
import jadex.bridge.service.types.awareness.AwarenessInfo;
import jadex.bridge.service.types.message.ICodec;
import jadex.commons.SReflect;
import jadex.commons.SUtil;
import jadex.commons.collection.ArrayBlockingQueue;
import jadex.commons.collection.IBlockingQueue;
import jadex.commons.collection.IBlockingQueue.ClosedException;
import jadex.commons.concurrent.TimeoutException;
import jadex.commons.transformation.STransformation;
import jadex.commons.transformation.binaryserializer.BinarySerializer;
import jadex.commons.transformation.binaryserializer.IErrorReporter;
import jadex.platform.service.message.MapSendTask;
import jadex.platform.service.message.transport.MessageEnvelope;
import jadex.platform.service.message.transport.codecs.CodecFactory;
import jadex.platform.service.message.transport.httprelaymtp.RelayConnectionManager;
import jadex.platform.service.message.transport.httprelaymtp.SRelay;
import jadex.xml.bean.JavaReader;


/**
 *  Basic relay functionality to be used with or without servlet.
 */
public class RelayHandler
{
	//-------- constants --------

	/** The directory for settings and statistics. */ 
	public final static File	SYSTEMDIR;
	
	static
	{
		File dir;
		// System.getProperty() does not return environment variables, but just server VM properties.
		String	home	= System.getenv("RELAY_HOME");
		if(home!=null)
		{
			dir	= new File(home);
		}
		else
		{
			if("true".equals(System.getProperty("relay.standalone")))
			{				
				dir	= new File(".", ".relaystats");
			}
			else
			{
				dir	= new File(System.getProperty("user.home"), ".relaystats");
			}
		}
		
		if(!dir.exists())
		{
			if(!dir.mkdirs())
			{
				getLogger().info("Cannot mkdirs: "+dir);
			}
		}
		else if(!dir.isDirectory())
		{
			throw new RuntimeException("Settings path '"+dir+"' is not a directory.");
		}
		SYSTEMDIR	= dir;
		
		getLogger().info("Relay settings directory (change with $RELAY_HOME): "+SYSTEMDIR.getAbsolutePath());
	}
	
	//-------- attributes --------
	
	/** The settings loaded from file. */
	protected RelayServerSettings	settings;
	
	/** The relay map (id -> queue for pending requests). */
	protected Map>	map;
	
	/** Info about connected platforms.*/
	protected Map	platforms;
	
	/** The available codecs for awareness infos (cached for speed). */
	protected Map	codecs;
	
	/** The default codecs (used for relay-to-relay communication). */
	protected ICodec[]	defcodecs;
	
	/** The peer list. */
	protected PeerList	peers;
	
	/** The statistics database (if any). */
	protected StatsDB	statsdb;
	
	/** The connection manager for communicating with remote peers. */
	protected RelayConnectionManager	conman;
	
	//-------- constructors --------

	/**
	 *  Initialize the handler.
	 */
	public RelayHandler()
	{
		this.map	= Collections.synchronizedMap(new HashMap>());
		this.platforms	= Collections.synchronizedMap(new LinkedHashMap());
		CodecFactory	cfac	= new CodecFactory();
		this.codecs	= cfac.getAllCodecs();
		this.defcodecs	= cfac.getDefaultCodecs();
		this.settings	= new RelayServerSettings();
		try
		{
			settings.loadSettings(new File(RelayHandler.SYSTEMDIR, "peer.properties"), true);
		}
		catch(Exception e)
		{
			getLogger().warning("Could not load relay settings: "+e);
		}
		this.peers	= new PeerList(this);
		this.statsdb	= StatsDB.createDB(settings.getId());
		this.conman	= new RelayConnectionManager();
		
		// Register communication classes with aliases
		STransformation.registerClass(MessageEnvelope.class);
		
		// Add initial peers.
		peers.addPeers(settings.getInitialPeers(), true);
	}
	
	/**
	 *  Cleanup on shutdown.
	 */
	public void dispose()
	{
		if(map!=null && !map.isEmpty())
		{
			for(Iterator> it=map.values().iterator(); it.hasNext(); )
			{
				IBlockingQueue	queue	= it.next();
				it.remove();
				List	items	= queue.setClosed(true);
				for(int i=0; i it=platforms.values().iterator(); it.hasNext(); )
			{
				PlatformInfo	pi	= it.next();
				pi.disconnect();
				if(statsdb!=null)
				{
					statsdb.save(pi);
				}
			}
		}
		
		if(statsdb!=null)
		{
			this.statsdb.shutdown();
		}
		
		this.peers.dispose();
		
		this.conman.dispose();
	}
	
	//-------- methods --------
	
	/**
	 *  Get the settings.
	 */
	public RelayServerSettings	getSettings()
	{
		return settings;
	}
	
	/**
	 *  Get the connection manager.
	 */
	public RelayConnectionManager	getConnectionManager()
	{
		return conman;
	}
	
	/**
	 *  Get the peer list.
	 */
	public PeerList getPeerList()
	{
		return peers;
	}
	
	/**
	 *  Called when a platform registers itself at the relay. 
	 *  Initializes required data structures, such that messages can be queued.
	 */
	public void initConnection(String id, String hostip, String hostname, String protocol)
	{
		PlatformInfo	info	= platforms.get(id);
		if(info==null)
		{
			info	= new PlatformInfo(id, settings.getId(), hostip, hostname, protocol);
			platforms.put(id, info);
		}
		else
		{
			// Throws exception, if reconnect not allowed (e.g. from different IP).
			info.reconnect(hostip, hostname, protocol, statsdb);
		}
		
		if(statsdb!=null)
		{
			statsdb.save(info);
		}
		
		IBlockingQueue	queue	= map.get(id);
		if(queue!=null)
		{
			// Close old queue to free old servlet request
			getLogger().info("Closing old queue due to reconnect of: "+id);
			List	items	= queue.setClosed(true);
			queue	= 	new ArrayBlockingQueue();
			// Add outstanding requests to new queue.
			for(int i=0; i();
		}
		map.put(id, queue);
		
		// Inform peers about connected platform.
		sendPlatformInfo(info);
				
//		// Set cache header to avoid interference of proxies (e.g. vodafone umts)
//		response.setHeader("Cache-Control", "no-cache, no-transform");
//		response.setHeader("Pragma", "no-cache");
		getLogger().info("Client connected: '"+id+"'");//, "+client.getSendBufferSize());
	}
				
	/**
	 *  Called when a platform registers itself at the relay. 
	 *  Blocks the thread until the platform disconnects.
	 */
	public void handleConnection(String id, OutputStream out)
	{
		PlatformInfo	info	= platforms.get(id);
		IBlockingQueue	queue	= map.get(id);
		
		try
		{
			// Ping to let client know that it is connected.
			out.write(SRelay.MSGTYPE_PING);
			out.flush();
//			response.flushBuffer();
			
			while(true)
			{
				try
				{
					// Get next request from queue.
					long	timeout	= (long)(SRelay.PING_DELAY*0.85);
//					getLogger().info("Waiting for message for: "+info.getId()+", timeout="+timeout);
					Message	msg	= queue.dequeue(timeout);	// Todo: make ping delay configurable on per client basis
					info.updateLastActiveTime();
//					System.out.println("sending data to:"+id);
					try
					{
						// Send message header.
						out.write(msg.getMessageType());
						
						// Copy message to output stream.
						long	start	= System.nanoTime();
						byte[]	buf	= new byte[8192];  
						int	len;
						int	cnt	= 0;
						while((len=msg.getContent().read(buf)) != -1)
						{
							out.write(buf, 0, len);
							cnt	+= len;
						}
						out.flush();
						info.addMessage(cnt, System.nanoTime()-start);
						msg.getFuture().setResult(null);
					}
					catch(Exception e)
					{
						msg.getFuture().setException(e);
						throw e;	// rethrow exception to end servlet execution for client.
					}
				}
				catch(TimeoutException te)
				{
//					getLogger().info("Activity timeout. Requesting ping from: "+info.getId()+", timeout="+info.testPlatformTimeout(SRelay.PING_DELAY));
					if(info.testPlatformTimeout(SRelay.PING_DELAY))
					{
						throw new TimeoutException("No platform activity in the last "+SRelay.PING_DELAY+" ms.");
					}
					
					// Send ping and continue loop.
//					System.out.println("pinging: "+id);
					out.write(SRelay.MSGTYPE_PING);
					out.flush();
				}
			}
		}
		catch(Exception e)
		{
			// exception on queue, when same platform reconnects or servlet is destroyed
			// exception on output stream, when client disconnects
			getLogger().info("Client disconnected: "+id+", "+e);
		}
		
		if(!queue.isClosed())
		{
			getLogger().info("Closing queue due disconnect of: "+id);
			List	items	= queue.setClosed(true);
			map.remove(id);
			PlatformInfo	platform	= platforms.remove(id);
			if(platform!=null)
			{
				platform.disconnect();
				if(statsdb!=null)
				{
					statsdb.save(platform);
				}
			}
			AwarenessInfo	awainfo	= platform!=null ? platform.getAwarenessInfo() : null;
			if(awainfo!=null)
			{
//				System.out.println("Sending offline info: "+id);
				awainfo.setState(AwarenessInfo.STATE_OFFLINE);
				sendAwarenessInfos(awainfo, platform.getPreferredCodecs(), true, false);
			}
			else if(platform!=null)
			{
				sendPlatformInfo(platform);
			}
	//		System.out.println("Removed from map ("+items.size()+" remaining items). New size: "+map.size());
			for(int i=0; i	queue	= map.get(targetid);
		if(queue!=null && targetpi!=null && (!protocol.equals("https") || targetpi.getScheme().equals("https")))
		{
			Message	msg	= new Message(SRelay.MSGTYPE_DEFAULT, in);
			queue.enqueue(msg);
			msg.getFuture().get(30000);	// todo: how to set a useful timeout value!?
		}
		else
		{
			throw new RuntimeException("message not sent: "+targetid+", "+targetpi+", "+queue);
		}
	}
	
	/**
	 *  Called when an awareness info is received from a connected platform.
	 */
	public void handleAwareness(InputStream in) throws Exception
	{
		// Read dummy target id.
		readString(in);
		
		// Read total message length.
		byte[]	len	= readData(in, 4);
		int	length	= SUtil.bytesToInt(len);
		
		// Read message and extract awareness info content.
		byte[] buffer = readData(in, length-1);
		MessageEnvelope	msg	= (MessageEnvelope)MapSendTask.decodeMessage(buffer, codecs, getClass().getClassLoader(), null);//IErrorReporter.IGNORE);
		ICodec[]	pcodecs	= MapSendTask.getCodecs(buffer, codecs);
		AwarenessInfo	info;
		if(SFipa.JADEX_RAW.equals(msg.getMessage().get(SFipa.LANGUAGE)))
		{
			info = (AwarenessInfo)msg.getMessage().get(SFipa.CONTENT);
		}
		else if(SFipa.JADEX_XML.equals(msg.getMessage().get(SFipa.LANGUAGE)))
		{
			info = (AwarenessInfo)JavaReader.objectFromByteArray((byte[])msg.getMessage().get(SFipa.CONTENT), getClass().getClassLoader(), IErrorReporter.IGNORE);
		}
		else //if(SFipa.JADEX_BINARY.equals(msg.getMessage().get(SFipa.LANGUAGE)))
		{
			info = (AwarenessInfo)BinarySerializer.objectFromByteArray((byte[])msg.getMessage().get(SFipa.CONTENT), null, null, getClass().getClassLoader(), null);
		}
		
		// Update platform awareness info.
		String	id	= info.getSender().getPlatformName();
		PlatformInfo	platform	= platforms.get(id);
		boolean	initial	= platform!=null && platform.getAwarenessInfo()==null && AwarenessInfo.STATE_ONLINE.equals(info.getState());
		if(platform!=null)
		{
			platform.updateLastActiveTime();
			platform.setAwarenessInfo(info);
			platform.setPreferredCodecs(pcodecs);
			
			if(statsdb!=null)
			{
				statsdb.save(platform);
			}				
		}
		
		sendAwarenessInfos(info, pcodecs, true, initial);
	}
	
	/**
	 *  Called when an offline status change is posted by a platform.
	 */
	public void handleOffline(String hostip, InputStream in) throws Exception
	{
		// Read platform id
		String	id	= readString(in);

		// Read total message length.	should be 0
		readData(in, 4);
		
		// Only accept status if from same IP
		PlatformInfo	pi	= platforms.get(id);
		if(pi==null)
		{
			throw new RuntimeException("No such platform: "+id);
		}
		else if(!hostip.equals(pi.getHostIP()))
		{
			throw new RuntimeException("Offline request from wrong IP: "+id+", "+hostip+", "+pi.getHostIP());			
		}
		else
		{
			PlatformInfo	platform	= platforms.remove(id);
			if(platform!=null)
			{
				platform.disconnect();
				if(statsdb!=null)
				{
					statsdb.save(platform);
				}
			}
			
			AwarenessInfo	awainfo	= platform!=null ? platform.getAwarenessInfo() : null;
			if(awainfo!=null)
			{
				awainfo.setState(AwarenessInfo.STATE_OFFLINE);
				sendAwarenessInfos(awainfo, platform.getPreferredCodecs(), true, false);
			}
			else if(platform!=null)
			{
				sendPlatformInfo(platform);
			}
			
			IBlockingQueue	queue	= map.get(id);
			if(queue!=null)
			{
				getLogger().info("Closing queue due offline notification of: "+id);
				List	items	= queue.setClosed(true);
				map.remove(id);
				for(int i=0; i	old	= new LinkedHashMap();
		for(PlatformInfo info: peer.getPlatformInfos())
		{
			if(info.getAwarenessInfo()!=null)
			{
				old.put(info.getId(), info);
			}
		}
		peer.clearPlatformInfos();
		
		// Send infos for currently connected platforms
		for(PlatformInfo info: infos)
		{
			peer.updatePlatformInfo(info);
			if(info.getAwarenessInfo()!=null)
			{
				sendAwarenessInfos(info.getAwarenessInfo(), pcodecs, false, false);
				old.remove(info.getId());
			}
		}
		
		// Send offline infos for remaining previous platforms.
		for(PlatformInfo info: old.values())
		{
			AwarenessInfo	awainfo	= info.getAwarenessInfo();
			awainfo.setState(AwarenessInfo.STATE_OFFLINE);
			sendAwarenessInfos(awainfo, pcodecs, false, false);
		}
	}
	
	/**
	 *  Send requested db entries.
	 */
	public void handleSyncRequest(String peerid, int startid, int cnt, OutputStream out) throws Exception
	{
		PlatformInfo[]	pi	= getStatisticsDB().getPlatformInfosForSync(peerid, startid, cnt);
		byte[]	entries	= MapSendTask.encodeMessage(pi, defcodecs, getClass().getClassLoader(), null);
		out.write(entries);
	}

	/**
	 *  Get the current platforms
	 */
	public PlatformInfo[]	getCurrentPlatforms()
	{
		// Fetch array to avoid concurrency problems
		return platforms.values().toArray(new PlatformInfo[0]);
	}
	
	/**
	 *  Get the statistics database (if any).
	 */
	public StatsDB	getStatisticsDB()
	{
		return this.statsdb;
	}
	
	/**
	 *  Get the current peers.
	 */
	public PeerHandler[]	getCurrentPeers()
	{
		return peers.getPeers();
	}

	/**
	 * Specifies the url under which this relay instance is reachable.
	 * @param url
	 */
	public void setUrl(String url)
	{
		settings.setUrl(url);
	}
	
	/**
	 *  Get the available servers as comma-separated list of URLs.
	 *  Also updates the known peers, if necessary.
	 *  @param requesturl	Public URL of this relay server as known from the received request.
	 *  @param peerurl	URL of a remote peer if sent as part of the request (or null).
	 *  @param peerstate	Latest DB id if sent as part of the request (or -1).
	 *  @param initial	True when remote peer recovers from failure (or false).
	 */
	public String	handleServersRequest(String requesturl, String peerurl, String peerid, int peerstate, boolean initial)
	{
		if(peerurl!=null)
		{
			PeerHandler	peer	= peers.addPeer(peerurl, peerid, peerstate);

			// Send own awareness infos to new peer.
			if(initial)
			{
				peer.setSent(true);
				sendPlatformInfos(peer, getCurrentPlatforms());
			}
		}
		return peers.getURLs(requesturl);
	}
	
	/**
	 *  Send a single platform info to all peer relay servers.
	 */
	public void	sendPlatformInfo(PlatformInfo info)
	{
		try
		{
			byte[]	peerinfo	= null;
			for(PeerHandler peer: peers.getPeers())
			{
//				if(peer.isConnected())
				{
					if(peerinfo==null)
					{
						peerinfo	= MapSendTask.encodeMessage(info, defcodecs, getClass().getClassLoader(), null);
					}
					peer.addDebugText(3, "Sending platform info to peer "+info.getId());
					conman.postMessage(peer.getUrl()+"platforminfo", new BasicComponentIdentifier(settings.getUrl()), new byte[][]{peerinfo});
					peer.addDebugText(3, "Sent platform info to peer "+info.getId());
				}
			}
		}
		catch(IOException e)
		{
			for(PeerHandler peer: peers.getPeers())
			{
				if(peer.isConnected())
				{
					peer.addDebugText(3, "Error sending platform info to peer: "+peer.getUrl()+"platforminfo, "+e);
				}
			}
			getLogger().warning("Error sending platform info to peer: "+e);
		}					
	}
	
	/**
	 *  Send platform infos to a peer relay server.
	 */
	public void	sendPlatformInfos(PeerHandler peer, PlatformInfo[] infos)
	{
		try
		{
			peer.addDebugText(3, "Sending platform infos to peer: "+infos.length);
			byte[]	peerinfo	= MapSendTask.encodeMessage(infos, defcodecs, getClass().getClassLoader(), null);
			conman.postMessage(RelayConnectionManager.httpAddress(peer.getUrl())+"platforminfos", new BasicComponentIdentifier(settings.getUrl()), new byte[][]{peerinfo});
			peer.addDebugText(3, "Sent platform infos.");
		}
		catch(IOException e)
		{
			peer.addDebugText(3, "Error sending platform infos to peer: "+peer.getUrl()+"platforminfos, "+e);
			getLogger().warning("Error sending platform infos to peer: "+peer.getUrl()+"platforminfos, "+e);
		}					
	}
	
	//-------- helper methods --------	

	/**
	 *  Send awareness messages for a new or changed awareness info.
	 */
	protected void	sendAwarenessInfos(AwarenessInfo awainfo, ICodec[] pcodecs, boolean local, boolean initial)
	{
//		System.out.println("sending awareness infos: "+awainfo.getSender().getPlatformName()+", "+platforms.size());
		pcodecs	= pcodecs!=null ? pcodecs : defcodecs;
		
		String	id	= awainfo.getSender().getPlatformName();
		PlatformInfo	platform	= platforms.get(id);
		
		// Ignore remote platforms if also connected local
		// (e.g. remote relay is down, platform reconnects at local relay,
		// afterwards local detects remote is down and wants to send offline info for already reconnected platform)
		if(platform==null || local)
		{
			byte[]	propinfo	= null;
			byte[]	nopropinfo	= null;
			
			Map.Entry>[] platformentries = map.entrySet().toArray(new Map.Entry[0]);
			for(int i=0; i




© 2015 - 2025 Weber Informatics LLC | Privacy Policy