package org.marketcetera.saclient.rpc;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.concurrent.GuardedBy;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.marketcetera.core.CloseableLock;
import org.marketcetera.module.ModuleInfo;
import org.marketcetera.module.ModuleURN;
import org.marketcetera.saclient.AbstractSAClient;
import org.marketcetera.saclient.ConnectionException;
import org.marketcetera.saclient.CreateStrategyParameters;
import org.marketcetera.saclient.SAClientParameters;
import org.marketcetera.saclient.SAClientVersion;
import org.marketcetera.saclient.rpc.RpcSAClient.Locale;
import org.marketcetera.saclient.rpc.RpcSAClient.LoginRequest;
import org.marketcetera.saclient.rpc.RpcSAClient.LoginResponse;
import org.marketcetera.saclient.rpc.RpcSAClient.LogoutRequest;
import org.marketcetera.saclient.rpc.RpcSAClient.RpcSAClientService;
import org.marketcetera.saclient.rpc.RpcSAClient.RpcSAClientService.BlockingInterface;
import org.marketcetera.util.log.SLF4JLoggerProxy;
import org.marketcetera.util.misc.ClassVersion;
import org.marketcetera.util.ws.ContextClassProvider;
import org.marketcetera.util.ws.tags.NodeId;
import org.marketcetera.util.ws.tags.SessionId;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.protobuf.RpcController;
import com.google.protobuf.ServiceException;
import com.googlecode.protobuf.pro.duplex.PeerInfo;
import com.googlecode.protobuf.pro.duplex.RpcClientChannel;
import com.googlecode.protobuf.pro.duplex.client.DuplexTcpClientPipelineFactory;
import com.googlecode.protobuf.pro.duplex.execute.RpcServerCallExecutor;
import com.googlecode.protobuf.pro.duplex.execute.ThreadPoolCallExecutor;
/* $License$ */
/**
* Provides an RPC implementation of {@link SAClient}.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: RpcSAClientImpl.java 16901 2014-05-11 16:14:11Z colin $
* @since 2.4.0
*/
@ClassVersion("$Id: RpcSAClientImpl.java 16901 2014-05-11 16:14:11Z colin $")
public class RpcSAClientImpl
extends AbstractSAClient
{
/**
* Create a new RpcSAClientImpl instance.
*
* @param inParameters an <code>SAClientParameters</code> value
*/
RpcSAClientImpl(SAClientParameters inParameters)
{
super(inParameters);
contextClassProvider = inParameters.getContextClassProvider();
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#getProviders()
*/
@Override
public List<ModuleURN> getProviders()
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.ProvidersResponse response = clientService.getProviders(controller,
RpcSAClient.ProvidersRequest.newBuilder().setSessionId(sessionId.getValue()).build());
List<ModuleURN> providers = Lists.newArrayList();
for(RpcSAClient.ModuleURN provider : response.getProviderList()) {
providers.add(new ModuleURN(provider.getValue()));
}
return providers;
} catch (ServiceException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#getInstances(org.marketcetera.module.ModuleURN)
*/
@Override
public List<ModuleURN> getInstances(ModuleURN inProviderURN)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.InstancesResponse response = clientService.getInstances(controller,
RpcSAClient.InstancesRequest.newBuilder().setSessionId(sessionId.getValue())
.setProvider(RpcSAClient.ModuleURN.newBuilder().setValue(inProviderURN.getValue())).build());
List<ModuleURN> instances = Lists.newArrayList();
for(RpcSAClient.ModuleURN instance : response.getInstanceList()) {
instances.add(new ModuleURN(instance.getValue()));
}
return instances;
} catch (ServiceException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#getModuleInfo(org.marketcetera.module.ModuleURN)
*/
@Override
public ModuleInfo getModuleInfo(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.ModuleInfoResponse response = clientService.getModuleInfo(controller,
RpcSAClient.ModuleInfoRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
ModuleInfo info = null;
if(response.hasInfo()) {
info = unmarshal(response.getInfo().getPayload());
}
return info;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#start(org.marketcetera.module.ModuleURN)
*/
@Override
public void start(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
clientService.start(controller,
RpcSAClient.StartRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
return;
} catch (ServiceException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#stop(org.marketcetera.module.ModuleURN)
*/
@Override
public void stop(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
clientService.stop(controller,
RpcSAClient.StopRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
return;
} catch (ServiceException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#delete(org.marketcetera.module.ModuleURN)
*/
@Override
public void delete(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
clientService.delete(controller,
RpcSAClient.DeleteRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
return;
} catch (ServiceException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#getProperties(org.marketcetera.module.ModuleURN)
*/
@Override
public Map<String,Object> getProperties(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.GetPropertiesResponse response = clientService.getProperties(controller,
RpcSAClient.GetPropertiesRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
Map<String,Object> properties = Maps.newHashMap();
for(RpcSAClient.Entry entry : response.getProperties().getEntryList()) {
String key = entry.getKey();
Object value = ((XmlValue)unmarshal(entry.getValue())).getValue();
properties.put(key,
value);
}
return properties;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#setProperties(org.marketcetera.module.ModuleURN, java.util.Map)
*/
@Override
public Map<String,Object> setProperties(ModuleURN inURN,
Map<String,Object> inProperties)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.Properties.Builder propertiesBuilder = RpcSAClient.Properties.newBuilder();
for(Map.Entry<String,Object> entry : inProperties.entrySet()) {
RpcSAClient.Entry.Builder entryBuilder = RpcSAClient.Entry.newBuilder();
entryBuilder.setKey(entry.getKey());
// note that this assumes that all values are marshallable
try {
entryBuilder.setValue(marshal(new XmlValue(entry.getValue())));
} catch (JAXBException e) {
throw new ServiceException(e);
}
propertiesBuilder.addEntry(entryBuilder.build());
}
RpcSAClient.SetPropertiesResponse response = clientService.setProperties(controller,
RpcSAClient.SetPropertiesRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue()))
.setProperties(propertiesBuilder.build()).build());
Map<String,Object> properties = Maps.newHashMap();
for(RpcSAClient.Entry entry : response.getProperties().getEntryList()) {
String key = entry.getKey();
Object value = ((XmlValue)unmarshal(entry.getValue())).getValue();
properties.put(key,
value);
}
return properties;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#createStrategy(org.marketcetera.saclient.CreateStrategyParameters)
*/
@Override
public ModuleURN createStrategy(CreateStrategyParameters inParameters)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.CreateStrategyResponse response = clientService.createStrategy(controller,
RpcSAClient.CreateStrategyRequest.newBuilder().setSessionId(sessionId.getValue())
.setCreateStrategyParameters(RpcSAClient.CreateStrategyParameters.newBuilder().setPayload(marshal(inParameters)).build()).build());
ModuleURN instance = new ModuleURN(response.getInstance().getValue());
return instance;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#getStrategyCreateParms(org.marketcetera.module.ModuleURN)
*/
@Override
public CreateStrategyParameters getStrategyCreateParms(ModuleURN inURN)
throws ConnectionException
{
failIfDisconnected();
try {
RpcSAClient.StrategyCreateParmsResponse response = clientService.getStrategyCreateParms(controller,
RpcSAClient.StrategyCreateParmsRequest.newBuilder().setSessionId(sessionId.getValue())
.setInstance(RpcSAClient.ModuleURN.newBuilder().setValue(inURN.getValue())).build());
CreateStrategyParameters params = unmarshal(response.getCreateStrategyParameters().getPayload());
return params;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.SAClient#sendData(java.lang.Object)
*/
@Override
public void sendData(Object inData)
throws ConnectionException
{
failIfDisconnected();
try {
// note that inData must be JAXB marshallable
clientService.sendData(controller,
RpcSAClient.SendDataRequest.newBuilder().setSessionId(sessionId.getValue())
.setPayload(marshal(new XmlValue(inData))).build());
return;
} catch (ServiceException | JAXBException e) {
throw wrapRemoteFailure(e);
}
}
/**
* Get the contextClassProvider value.
*
* @return a <code>ContextClassProvider</code> value
*/
public ContextClassProvider getContextClassProvider()
{
return contextClassProvider;
}
/**
* Sets the contextClassProvider value.
*
* @param inContextClassProvider a <code>ContextClassProvider</code> value
*/
public void setContextClassProvider(ContextClassProvider inContextClassProvider)
{
contextClassProvider = inContextClassProvider;
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.AbstractSAClient#doStart()
*/
@Override
protected void doStart()
{
try {
synchronized(contextLock) {
context = JAXBContext.newInstance(contextClassProvider==null?new Class<?>[0]:contextClassProvider.getContextClasses());
marshaller = context.createMarshaller();
unmarshaller = context.createUnmarshaller();
}
startService();
heartbeatFuture = heartbeatService.scheduleAtFixedRate(new HeartbeatMonitor(),
heartbeatInterval,
heartbeatInterval,
TimeUnit.MILLISECONDS);
} catch (IOException | ServiceException | JAXBException e) {
throw new RuntimeException(e);
}
}
/* (non-Javadoc)
* @see org.marketcetera.saclient.AbstractSAClient#doStop()
*/
@Override
protected void doStop()
{
if(heartbeatFuture != null) {
try {
heartbeatFuture.cancel(true);
} catch (Exception ignored) {}
}
}
/**
* Starts the remote service.
*
* @throws IOException if an error occurs starting the service
* @throws ServiceException if an error occurs starting the service
*/
private void startService()
throws IOException, ServiceException
{
try(CloseableLock startLock = CloseableLock.create(serviceLock.writeLock())) {
startLock.lock();
SLF4JLoggerProxy.debug(this,
"Connecting to RPC server at {}:{}", //$NON-NLS-1$
parameters.getHostname(),
parameters.getPort());
PeerInfo server = new PeerInfo(parameters.getHostname(),
parameters.getPort());
DuplexTcpClientPipelineFactory clientFactory = new DuplexTcpClientPipelineFactory();
executor = new ThreadPoolCallExecutor(1,
10);
clientFactory.setRpcServerCallExecutor(executor);
clientFactory.setConnectResponseTimeoutMillis(10000);
clientFactory.setCompression(true);
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(new NioEventLoopGroup());
bootstrap.handler(clientFactory);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY,
true);
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
10000);
bootstrap.option(ChannelOption.SO_SNDBUF,
1048576);
bootstrap.option(ChannelOption.SO_RCVBUF,
1048576);
channel = clientFactory.peerWith(server,
bootstrap);
clientService = RpcSAClientService.newBlockingStub(channel);
controller = channel.newRpcController();
java.util.Locale currentLocale = java.util.Locale.getDefault();
LoginRequest loginRequest = LoginRequest.newBuilder()
.setAppId(SAClientVersion.APP_ID.getValue())
.setVersionId(SAClientVersion.APP_ID_VERSION.getVersionInfo())
.setClientId(NodeId.generate().getValue())
.setLocale(Locale.newBuilder()
.setCountry(currentLocale.getCountry()==null?"":currentLocale.getCountry()) //$NON-NLS-1$
.setLanguage(currentLocale.getLanguage()==null?"":currentLocale.getLanguage()) //$NON-NLS-1$
.setVariant(currentLocale.getVariant()==null?"":currentLocale.getVariant()).build()) //$NON-NLS-1$
.setUsername(parameters.getUsername())
.setPassword(new String(parameters.getPassword())).build();
LoginResponse loginResponse = clientService.login(controller,
loginRequest);
sessionId = new SessionId(loginResponse.getSessionId());
connectionStatusChanged(isRunning(),
true);
}
}
/**
* Stops the remote service.
*/
private void stopService()
{
try(CloseableLock stopLock = CloseableLock.create(serviceLock.writeLock())) {
stopLock.lock();
try {
clientService.logout(controller,
LogoutRequest.newBuilder().setSessionId(sessionId.getValue()).build());
} catch (Exception ignored) {}
if(executor != null) {
try {
executor.shutdownNow();
} catch (Exception ignored) {}
}
if(channel != null) {
try {
channel.close();
} catch (Exception ignored) {}
}
} finally {
executor = null;
controller = null;
clientService = null;
channel = null;
sessionId = null;
running.set(false);
}
}
/**
* Marshals the given object to an XML stream.
*
* @param inObject an <code>Object</code> value
* @return a <code>String</code> value
* @throws JAXBException if an error occurs marshalling the data
*/
private String marshal(Object inObject)
throws JAXBException
{
StringWriter output = new StringWriter();
synchronized(contextLock) {
marshaller.marshal(inObject,
output);
}
return output.toString();
}
/**
* Unmarshals an object from the given XML stream.
*
* @param inData a <code>String</code> value
* @return a <code>Clazz</code> value
* @throws JAXBException if an error occurs unmarshalling the data
*/
@SuppressWarnings("unchecked")
private <Clazz> Clazz unmarshal(String inData)
throws JAXBException
{
synchronized(contextLock) {
return (Clazz)unmarshaller.unmarshal(new StringReader(inData));
}
}
/**
* Sends heartbeats and monitors the responses.
*
* <p>This class also manages reconnection, if necessary.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: RpcSAClientImpl.java 16901 2014-05-11 16:14:11Z colin $
* @since 2.4.0
*/
@ClassVersion("$Id: RpcSAClientImpl.java 16901 2014-05-11 16:14:11Z colin $")
private class HeartbeatMonitor
implements Runnable
{
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run()
{
if(!isRunning() && !shutdownRequested.get()) {
try {
stopService();
startService();
} catch (Exception ignored) {}
}
try(CloseableLock heartbeatLock = CloseableLock.create(serviceLock.readLock())) {
heartbeatLock.lock();
clientService.heartbeat(controller,
RpcSAClient.HeartbeatRequest.newBuilder().setId(System.nanoTime()).build());
} catch (Exception e) {
// heartbeat failed for some reason
SLF4JLoggerProxy.debug(RpcSAClientImpl.this,
e,
"Heartbeat failed"); //$NON-NLS-1$
connectionStatusChanged(isRunning(),
false);
}
}
}
/**
* guards access to RPC service objects
*/
private final ReadWriteLock serviceLock = new ReentrantReadWriteLock();
/**
* provides access to RPC services
*/
private BlockingInterface clientService;
/**
* executes the nitty-gritty of the calls
*/
private RpcServerCallExecutor executor;
/**
* channel over which calls are made
*/
private RpcClientChannel channel;
/**
* controller responsible for the RPC connection
*/
private RpcController controller;
/**
* guards access to JAXB context objects
*/
private final Object contextLock = new Object();
/**
* context used to serialize and unserialize messages as necessary
*/
@GuardedBy("contextLock")
private JAXBContext context;
/**
* marshals messages
*/
@GuardedBy("contextLock")
private Marshaller marshaller;
/**
* unmarshals messages
*/
@GuardedBy("contextLock")
private Unmarshaller unmarshaller;
/**
* provides context classes for marshalling/unmarshalling, may be <code>null</code>
*/
private ContextClassProvider contextClassProvider;
/**
* session ID value for this connection, may be <code>null</code> if the connection is inactive
*/
private SessionId sessionId;
/**
* indicates that a shutdown has been requested
*/
private final AtomicBoolean shutdownRequested = new AtomicBoolean(false);
/**
* stores a handle to the heartbeat scheduled job
*/
private ScheduledFuture<?> heartbeatFuture;
/**
* interval at which to execute heartbeats
*/
private long heartbeatInterval = 10000;
/**
* executes heartbeats
*/
private final ScheduledExecutorService heartbeatService = Executors.newScheduledThreadPool(1);
}