/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.remoting;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.jboss.remoting.callback.InvokerCallbackHandler;
import org.jboss.remoting.invocation.InternalInvocation;
import org.jboss.remoting.invocation.OnewayInvocation;
import org.jboss.remoting.marshal.Marshaller;
import org.jboss.remoting.marshal.UnMarshaller;
import org.jboss.remoting.stream.StreamServer;
import org.jboss.remoting.transport.ClientInvoker;
import org.jboss.util.id.GUID;
import org.jboss.util.threadpool.BasicThreadPool;
import org.jboss.util.threadpool.BlockingMode;
import org.jboss.util.threadpool.ThreadPool;
/**
* Client is a convience class for invoking remote methods for a given subsystem.
* It is intended to be the main user interface for making remote invocation
* on the client side.
*
* @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
* @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
* @version $Revision: 1.27 $
*/
public class Client implements Serializable
{
/**
* Key to be used when tracking callback listeners.
*/
public static final String LISTENER_ID_KEY = "listenerId";
/**
* Specifies the default number of work threads in the pool for
* executing one way invocations on the client.
* Value is 10.
*/
public static final int MAX_NUM_ONEWAY_THREADS = 10;
/**
* The key to use for the metadata Map passed when making a invoke() call
* and wish for the invocation payload to be sent as is and not wrapped
* within a remoting invocation request object. This should be used
* when want to make direct calls on systems outside of remoting
* (e.g. making a http POST request to a web service).
*/
public static final String RAW = "RAW_PAYLOAD";
/**
* Key for the configuration map passed to the Client constructor
* to indicate that client should not make initial request to establish
* lease with server. May be needed when connecting to non-remoting server
* (especially if using http client only). The value for this should be
* either a String that java.lang.Boolean can evaluate or a java.lang.Boolean.
*/
public static final String ENABLE_LEASE = "ENABLE_LEASE";
/**
* Indicated the max number of threads used within oneway thread pool.
*/
private int maxNumberThreads = MAX_NUM_ONEWAY_THREADS;
private static final Logger log = Logger.getLogger(Client.class);
private ClientInvoker invoker;
private ClassLoader classloader;
private String subsystem;
private String sessionId = new GUID().toString();
private ThreadPool onewayThreadPool;
private ConnectionValidator connectionValidator = null;
private LeasePinger leasePinger = null;
private Map configuration = null;
private boolean enableLease = false;
private long leasePeriod = -1;
/**
* Constructs a remoting client with intended target server specified via the lcoator,
* without specifing a remote subsystem or including any metadata.
* Same as calling Client(locator, null, null);
* @param locator
* @throws Exception
*/
public Client(InvokerLocator locator) throws Exception
{
this(locator, null, null);
}
/**
* Constructs a remoting client with intended target server specified via the locator
* and configuration metadata. The metadata supplied will be used when creating client
* invoker (in the case specific data is required) and also for passing along additional
* data to connection listeners on the server side in the case that the client fails, will
* be able to use this extra information when notified.
* @param locator
* @param configuration
* @throws Exception
*/
public Client(InvokerLocator locator, Map configuration) throws Exception
{
this(locator, null, configuration);
}
/**
* Constructs a remoting client with intended target server specified via the locator
* and intended subsystem on server for invocations to be routed to.
* @param locator
* @param subsystem
* @throws Exception
*/
public Client(InvokerLocator locator, String subsystem)
throws Exception
{
this(locator, subsystem, null);
}
/**
* Constructs a remoting client with intended target server specified via the locator, intended subsystem
* on the server for invocations to be routed to, and configuration metadata.
* The metadata supplied will be used when creating client
* invoker (in the case specific data is required) and also for passing along additional
* data to connection listeners on the server side in the case that the client fails, will
* be able to use this extra information when notified.
* @param locator
* @param subsystem
* @param configuration
* @throws Exception
*/
public Client(InvokerLocator locator, String subsystem, Map configuration)
throws Exception
{
this(Thread.currentThread().getContextClassLoader(), locator, subsystem, configuration);
}
/**
* Constructs a remoting client with intended target server specified via the locator, intended subsystem
* on the server for invocations to be routed to, and configuration metadata.
* The metadata supplied will be used when creating client
* invoker (in the case specific data is required) and also for passing along additional
* data to connection listeners on the server side in the case that the client fails, will
* be able to use this extra information when notified.
* @param cl - the classloader that should be used by remoting
* @param locator
* @param subsystem
* @param configuration
* @throws Exception
* @deprecated This constructor should not be used any more as will no longer take into
* account the classloader specified as a parameter.
*/
public Client(ClassLoader cl, InvokerLocator locator, String subsystem, Map configuration)
throws Exception
{
this(cl, InvokerRegistry.createClientInvoker(locator, configuration), subsystem);
this.configuration = configuration;
}
/**
* Constructs a remoting client with intended target server specified via the locator
* and intended subsystem on server for invocations to be routed to.
* @param cl
* @param invoker
* @param subsystem
* @throws Exception
* @deprecated This constructor should not be used any more as will no longer take into
* account the classloader specified as a parameter.
*/
public Client(ClassLoader cl, ClientInvoker invoker, String subsystem)
throws Exception
{
this.classloader = cl;
this.subsystem = subsystem == null ? null : subsystem.toUpperCase();
this.invoker = invoker;
}
/**
* Adds a connection listener that will be notified if/when the connection
* to the server fails while the client is idle (no calls being made).
* The default behavior is to ping for connection every two seconds.
*
* @param listener
*/
public void addConnectionListener(ConnectionListener listener)
{
if(connectionValidator == null)
{
connectionValidator = new ConnectionValidator(this);
}
connectionValidator.addConnectionListener(listener);
}
/**
* Adds a connection listener that will be notified if/when the connection
* to the server fails while the client is idle (no calls being made).
* The current behavior is to ping the server periodically. The time period
* is defined by the pingPeriod (which should be in milliseconds).
*
* @param listener
*/
public void addConnectionListener(ConnectionListener listener, int pingPeriod)
{
if(connectionValidator == null)
{
connectionValidator = new ConnectionValidator(this, pingPeriod);
}
connectionValidator.addConnectionListener(listener);
}
/**
* Removes specified connection listener. Will return true if it has
* already been registered, false otherwise.
*
* @param listener
* @return
*/
public boolean removeConnectionListener(ConnectionListener listener)
{
return connectionValidator.removeConnectionListener(listener);
}
/**
* This will set the session id used when making invocations on
* server invokers. There is a default unique id automatically
* generated for each Client instance, so unless you have a good reason to set
* this, do not set this.
*
* @param sessionId
*/
public void setSessionId(String sessionId)
{
this.sessionId = sessionId;
}
/**
* Gets the configuration map passed when constructing
* this object.
* @return
*/
public Map getConfiguration()
{
return configuration;
}
/**
* Gets the session id used when making invocations on server invokers.
* This is the id that will be used for tracking client connections on
* the server side, to include client failures that are sent to
* connection listeners on the server side.
* @return
*/
public String getSessionId()
{
return this.sessionId;
}
/**
* Indicates if the underlying transport has been connected to
* the target server.
* @return
*/
public boolean isConnected()
{
return (this.invoker != null && this.invoker.isConnected());
}
/**
* Will cause the underlying transport to make connection to
* the target server. This is important for any stateful transports, like socket or multiplex.
* This is also when a client lease with the server is started.
* @throws Exception
*/
public void connect() throws Exception
{
if(!isConnected())
{
connect(invoker);
}
}
private void connect(ClientInvoker invoker)
{
invoker.connect();
setupClientLease(invoker);
}
private void setupClientLease(ClientInvoker invoker)
{
// start with checking the locator url for hint as to if should do initial lease ping
if(invoker != null)
{
InvokerLocator locator = invoker.getLocator();
Map locatorParams = locator.getParameters();
if(locatorParams != null)
{
String leaseValue = (String)locatorParams.get(InvokerLocator.CLIENT_LEASE);
if(leaseValue != null && leaseValue.length() > 0)
{
enableLease = Boolean.valueOf(leaseValue).booleanValue();
}
String leasePeriodValue = (String)locatorParams.get(InvokerLocator.CLIENT_LEASE_PERIOD);
if(leasePeriodValue != null && leasePeriodValue.length() > 0)
{
try
{
leasePeriod = Long.parseLong(leasePeriodValue);
}
catch(NumberFormatException e)
{
log.warn("Could not convert client lease period value (" + leasePeriodValue + ") to a number.");
}
}
}
}
if(configuration != null)
{
Object val = configuration.get(ENABLE_LEASE);
if(val != null)
{
if(val instanceof Boolean)
{
enableLease = ((Boolean) val).booleanValue();
}
else if(val instanceof String)
{
enableLease = Boolean.valueOf((String) val).booleanValue();
}
else
{
log.warn("Can not evaluate " + ENABLE_LEASE + " value (" + val + ") as a boolean type.");
}
}
}
if(enableLease)
{
Object ret = null;
try
{
ret = invoker.invoke(new InvocationRequest(sessionId, subsystem, "$PING$", configuration, null, null));
if(ret instanceof InvocationResponse)
{
InvocationResponse resp = (InvocationResponse) ret;
Boolean shouldLease = (Boolean)resp.getResult();
if(shouldLease.booleanValue())
{
// if lease period not set via locator param, check value returned by server
if(leasePeriod < 0)
{
Map respMap = resp.getPayload();
if(respMap != null)
{
Long leaseTimeoutValue = (Long) respMap.get("clientLeasePeriod");
leasePeriod = leaseTimeoutValue.longValue();
}
}
if(leasePeriod > 0)
{
if(leasePinger == null)
{
leasePinger = new LeasePinger(this);
leasePinger.startPing(leasePeriod);
}
}
}
}
}
catch(Throwable throwable)
{
log.error("Error setting up client lease.", throwable);
}
}
}
/**
* Disconnects the underlying transport from the target server.
* Also notifies the target server to terminate client lease. Is important
* that this method is called when no longer using the remoting client. Otherwise
* resource will not be cleaned up and if the target server requires a lease, it
* will be maintained in the background.
*/
public void disconnect()
{
if(leasePinger != null)
{
try
{
invoker.invoke(new InvocationRequest(sessionId, subsystem, "$DISCONNECT$", null, null, null));
}
catch(Throwable throwable)
{
log.error("Error sending disconnect to server to end client lease.", throwable);
}
leasePinger.stopPing();
}
this.invoker.disconnect();
}
/**
* Get the client invoker (transport implementation).
* @return
*/
public ClientInvoker getInvoker()
{
return invoker;
}
/**
* Set the client invoker (transport implementation)
* @param invoker
*/
public void setInvoker(ClientInvoker invoker)
{
this.invoker = invoker;
}
/**
* Gets the subsystem being used when routing
* invocation request on the server side.
* @return
*/
public String getSubsystem()
{
return subsystem;
}
/**
* Sets the subsystem being used when routing invocation requests
* on the server side. Specifing a subsystem is only needed when
* server has multiple handlers registered (which will each have their
* own associated subsystem).
* @param subsystem
*/
public void setSubsystem(String subsystem)
{
this.subsystem = subsystem;
}
/**
* Invokes the server invoker handler with the payload parameter passed.
* Same as calling invoke(param, null);
* @param param
* @return
* @throws Throwable
*/
public Object invoke(Object param) throws Throwable
{
return invoke(param, null);
}
/**
* invoke the method remotely
*
* @param param - payload for the server invoker handler
* @param metadata - any extra metadata that may be needed by the transport (i.e. GET or POST if using
* http invoker) or if need to pass along extra data to the server invoker handler.
* @return
* @throws Throwable
*/
public Object invoke(Object param, Map metadata)
throws Throwable
{
return invoke(param, metadata, null);
}
private Object invoke(Object param, Map metadata, InvokerLocator callbackServerLocator)
throws Throwable
{
/**
* Using a local variable for the invoker as work around so don't have
* to sync method (and take performance hit)
* Although this may cause having multiple instances of invoker in existance at
* one time, should avoid having reference changed by another thread while in
* execution path for method.
*/
ClientInvoker localInvoker = invoker;
if(localInvoker != null)
{
/**
* This is here due to way the InvokerRegistry works. Since it will cache references to
* client invokers it creates based on locator, it is possible that this instance will
* have a reference to the same instance of the invoker as another client. So is possible
* that client will disconnect, which will cause the invoker to be disconnected. Therefore,
* if this were to happen, we would create a new one by calling the createClientInvoker() method.
*/
if(localInvoker.isConnected() == false)
{
log.debug("invoke called, but our invoker is disconnected, discarding and fetching another fresh invoker for: " + invoker.getLocator());
localInvoker = InvokerRegistry.createClientInvoker(localInvoker.getLocator(), configuration);
connect(localInvoker);
}
}
else
{
throw new Exception("Can not perform invoke because invoker is null.");
}
Object ret = localInvoker.invoke(new InvocationRequest(sessionId, subsystem, param, metadata, null, callbackServerLocator));
this.invoker = localInvoker;
return ret;
}
/**
* Will invoke a oneway call to server without a return object. This should be used when not expecting a
* return value from the server and wish to achieve higher performance, since the client will not wait for
* a return.
* <b>
* This is done one of two ways. The first is to pass true as the clientSide param. This will cause the
* execution of the remote call to be excuted in a new thread on the client side and will return the calling thread
* before making call to server side. Although, this is optimal for performance, will not know about any problems
* contacting server.
* <p/>
* The second, is to pass false as the clientSide param. This will allow the current calling thread to make
* the call to the remote server, at which point, the server side processing of the thread will be executed on
* the remote server in a new executing thread and the client thread will return. This is a little slower, but
* will know that the call made it to the server.
*
* @param param
* @param sendPayload
* @param clientSide
*/
public void invokeOneway(final Object param, final Map sendPayload, boolean clientSide) throws Throwable
{
if(clientSide)
{
ThreadPool threadPool = getOnewayThreadPool();
Runnable onewayRun = new Runnable()
{
public void run()
{
try
{
invoke(param, sendPayload);
}
catch(Throwable e)
{
// throw away exception since can't get it back to original caller
log.error("Error executing client oneway invocation request: " + param, e);
}
}
};
threadPool.run(onewayRun);
}
else
{
OnewayInvocation invocation = new OnewayInvocation(param);
invoke(invocation, sendPayload);
}
}
/**
* Sets the maximum number of threads to use within client pool for
* one way invocations on the client side (meaning oneway invocation
* is handled by thread in this pool and user's call returns immediately)
* Default value is MAX_NUM_ONEWAY_THREADS.
* @param numOfThreads
*/
public void setMaxNumberOfThreads(int numOfThreads)
{
this.maxNumberThreads = numOfThreads;
}
/**
* Gets the maximum number of threads to use within client pool for
* one way invocations on the client side (meaning oneway invocation
* is handled by thread in this pool and user's call returns immediately)
* Default value is MAX_NUM_ONEWAY_THREADS.
* @return
*/
public int getMaxNumberOfThreads()
{
return this.maxNumberThreads;
}
/**
* Gets the thread pool being used for making
* one way invocations on the client side.
* If one has not be specifically set via configuration
* or call to set it, will always return instance of
* org.jboss.util.threadpool.BasicThreadPool.
* @return
*/
public ThreadPool getOnewayThreadPool()
{
if(onewayThreadPool == null)
{
BasicThreadPool pool = new BasicThreadPool("JBossRemoting Client Oneway");
pool.setMaximumPoolSize(maxNumberThreads);
pool.setBlockingMode(BlockingMode.WAIT);
onewayThreadPool = pool;
}
return onewayThreadPool;
}
/**
* Sets the thread pool to be used for making
* one way invocations on the client side.
* @param pool
*/
public void setOnewayThreadPool(ThreadPool pool)
{
this.onewayThreadPool = pool;
}
/**
* Same as calling invokeOneway(Object param, Map sendPayload, boolean clientSide) with
* clientSide param being false and a null sendPayload. Therefore, client thread will not return till it has made
* remote call.
*
* @param param
*/
public void invokeOneway(Object param) throws Throwable
{
invokeOneway(param, null);
}
/**
* Same as calling invokeOneway(Object param, Map sendPayload, boolean clientSide) with
* clientSide param being false. Therefore, client thread will not return till it has made
* remote call.
*
* @param param
* @param sendPayload
*/
public void invokeOneway(Object param, Map sendPayload) throws Throwable
{
invokeOneway(param, sendPayload, false);
}
/**
* Adds the specified handler as a callback listener for pull (sync) callbacks.
* The invoker server will then collect the callbacks for this specific handler.
* The callbacks can be retrieved by calling the getCallbacks() method.
* Note: this will cause the client invoker's client locator to be set to null.
*
* @param callbackHandler
* @throws Throwable
*/
public void addListener(InvokerCallbackHandler callbackHandler) throws Throwable
{
addListener(callbackHandler, null);
}
/**
* Adds the specified handler as a callback listener for push (async) callbacks.
* The invoker server will then callback on this handler (via the server invoker
* specified by the clientLocator) when it gets a callback from the server handler.
* Note: passing a null clientLocator will cause the client invoker's client
* locator to be set to null.
*
* @param callbackHandler
* @param clientLocator
* @throws Throwable
*/
public void addListener(InvokerCallbackHandler callbackHandler,
InvokerLocator clientLocator) throws Throwable
{
addListener(callbackHandler, clientLocator, null);
}
/**
* Adds the specified handler as a callback listener for push (async) callbacks.
* The invoker server will then callback on this handler (via the server invoker
* specified by the clientLocator) when it gets a callback from the server handler.
* Note: passing a null clientLocator will cause the client invoker's client
* locator to be set to null.
*
* @param callbackHandler interface to call on with callback
* @param clientLocator locator for callback server to callback on
* @param callbackHandlerObject will be included in the callback object passed upon callback
* @throws Throwable
*/
public void addListener(InvokerCallbackHandler callbackHandler,
InvokerLocator clientLocator, Object callbackHandlerObject) throws Throwable
{
if(callbackHandler != null)
{
Map metadata = createListenerMetadata(callbackHandler);
String listenerId = (String) metadata.get(LISTENER_ID_KEY);
invoker.addClientLocator(listenerId, clientLocator);
if(clientLocator != null)
{
Client client = new Client(clientLocator, subsystem);
client.setSessionId(getSessionId());
client.connect();
client.invoke(new InternalInvocation(InternalInvocation.ADDCLIENTLISTENER,
new Object[]{callbackHandler, callbackHandlerObject}),
metadata);
client.disconnect();
}
// now call server to add listener
invoke(new InternalInvocation(InternalInvocation.ADDLISTENER, null), metadata, clientLocator);
}
else
{
throw new NullPointerException("InvokerCallbackHandler to be added as a listener can not be null.");
}
}
private Map createListenerMetadata(InvokerCallbackHandler callbackHandler)
{
String listenerId = String.valueOf(callbackHandler.hashCode());
Map metadata = new HashMap();
metadata.put(LISTENER_ID_KEY, listenerId);
return metadata;
}
/**
* Removes callback handler as a callback listener from the server (and client in
* the case that it was setup to receive async callbacks). See addListener().
*
* @param callbackHandler
* @throws Throwable
*/
public void removeListener(InvokerCallbackHandler callbackHandler) throws Throwable
{
if(callbackHandler != null)
{
Map metadata = createListenerMetadata(callbackHandler);
String listenerId = (String) metadata.get(LISTENER_ID_KEY);
// connect to the given client locator and remove handler as listener
InvokerLocator locator = invoker.getClientLocator(listenerId);
if(locator != null) // async callback
{
Client client = new Client(locator, subsystem);
client.setSessionId(getSessionId());
client.connect();
client.invoke(new InternalInvocation(InternalInvocation.REMOVECLIENTLISTENER,
new Object[]{callbackHandler}),
metadata);
client.disconnect();
}
// now call server to remove listener
invoke(new InternalInvocation(InternalInvocation.REMOVELISTENER, null), metadata);
}
else
{
throw new NullPointerException("Can not remove null InvokerCallbackHandler listener.");
}
}
/**
* Gets the callbacks for specified callback handler. The handler is required because an id is generated
* for each handler. So if have two callback handlers registered with the same server, no other way to know
* for which handler to get the callbacks for.
*
* @param callbackHandler
* @return
* @throws Throwable
*/
public List getCallbacks(InvokerCallbackHandler callbackHandler) throws Throwable
{
if(callbackHandler != null)
{
Map metadata = createListenerMetadata(callbackHandler);
return (List) invoke(new InternalInvocation(InternalInvocation.GETCALLBACKS, null), metadata);
}
else
{
throw new NullPointerException("Can not remove null InvokerCallbackHandler listener.");
}
}
/**
* Sets the marshaller implementation that should be used by the
* client invoker (transport). This overrides the client's default
* marshaller (or any set within configuration).
* @param marshaller
*/
public void setMarshaller(Marshaller marshaller)
{
if(invoker != null && marshaller != null)
{
invoker.setMarshaller(marshaller);
}
}
/**
* Sets the unmarshaller implementation that should be used
* by the client invoker (transport). This overrides the client's default
* unmarshaller (or any set within configuration).
* @param unmarshaller
*/
public void setUnMarshaller(UnMarshaller unmarshaller)
{
if(invoker != null && unmarshaller != null)
{
invoker.setUnMarshaller(unmarshaller);
}
}
/**
* Takes an inputstream and wraps a server around. Then calls the target
* remoting server and passes a proxy for an inputstream to the server's handler.
* When the server handler calls on this proxy, it will call back on this server
* wrapped around this inputstream.
*
* @param inputStream
* @param param invocation payload
* @return the return value from the invocation
* @throws Throwable
*/
public Object invoke(InputStream inputStream, Object param) throws Throwable
{
StreamServer streamServer = new StreamServer(inputStream);
String locator = streamServer.getInvokerLocator();
// now call on target server and pass locator for stream callbacks
return invoke(new InternalInvocation(InternalInvocation.ADDSTREAMCALLBACK, new Object[]{locator, param}), null);
}
}