/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.livedata.cogda.server; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.fudgemsg.FudgeContext; import org.fudgemsg.FudgeMsg; import org.fudgemsg.FudgeMsgEnvelope; import org.fudgemsg.mapping.FudgeDeserializer; import org.fudgemsg.mapping.FudgeSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.core.user.UserAccount; import com.opengamma.id.ExternalId; import com.opengamma.livedata.LiveDataSpecification; import com.opengamma.livedata.LiveDataValueUpdate; import com.opengamma.livedata.UserPrincipal; import com.opengamma.livedata.cogda.msg.CogdaCommandResponseResult; import com.opengamma.livedata.cogda.msg.CogdaLiveDataBuilderUtil; import com.opengamma.livedata.cogda.msg.CogdaLiveDataCommandResponseMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSnapshotRequestBuilder; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSnapshotRequestMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSnapshotResponseMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSubscriptionRequestBuilder; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSubscriptionRequestMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataSubscriptionResponseMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataUnsubscribeBuilder; import com.opengamma.livedata.cogda.msg.CogdaLiveDataUnsubscribeMessage; import com.opengamma.livedata.cogda.msg.CogdaLiveDataUpdateBuilder; import com.opengamma.livedata.cogda.msg.CogdaLiveDataUpdateMessage; import com.opengamma.livedata.cogda.msg.CogdaMessageType; import com.opengamma.livedata.cogda.msg.ConnectionRequestBuilder; import com.opengamma.livedata.cogda.msg.ConnectionRequestMessage; import com.opengamma.livedata.cogda.msg.ConnectionResponseBuilder; import com.opengamma.livedata.cogda.msg.ConnectionResponseMessage; import com.opengamma.livedata.cogda.msg.ConnectionResult; import com.opengamma.livedata.server.LastKnownValueStore; import com.opengamma.transport.FudgeConnection; import com.opengamma.transport.FudgeConnectionStateListener; import com.opengamma.transport.FudgeMessageReceiver; import com.opengamma.transport.FudgeMessageSender; import com.opengamma.util.ArgumentChecker; /** * The object which holds a particular connection to a Cogda client for * a particular {@link CogdaLiveDataServer}. */ public class CogdaClientConnection implements FudgeConnectionStateListener, FudgeMessageReceiver { /** * Subscribe to a stream */ public static final String SUBSCRIBE = "subscribe"; /** * Snapshot the state of the world */ public static final String SNAPSHOT = "snapshot"; private static final Logger s_logger = LoggerFactory.getLogger(CogdaClientConnection.class); private final FudgeContext _fudgeContext; private final CogdaLiveDataServer _server; private final FudgeMessageSender _messageSender; // REVIEW kirk 2013-03-27 -- The only reason why _subscriptions exists is to act as // a quick pass on whether the client is subscribed to a specification. // This is to avoid going into a locking state waiting for _valuesToSend. private final ConcurrentMap<LiveDataSpecification, Boolean> _subscriptions = new ConcurrentHashMap<LiveDataSpecification, Boolean>(); private final Map<LiveDataSpecification, FudgeMsg> _valuesToSend = new HashMap<LiveDataSpecification, FudgeMsg>(); private final Lock _writerLock = new ReentrantLock(); private final Lock _valuesToSendLock = new ReentrantLock(); private UserPrincipal _userPrincipal; private UserAccount _user; public CogdaClientConnection(FudgeContext fudgeContext, CogdaLiveDataServer server, FudgeConnection connection) { ArgumentChecker.notNull(fudgeContext, "fudgeContext"); ArgumentChecker.notNull(server, "server"); ArgumentChecker.notNull(connection, "fudgeConnection"); _fudgeContext = fudgeContext; _server = server; connection.setConnectionStateListener(this); connection.setFudgeMessageReceiver(this); _messageSender = connection.getFudgeMessageSender(); } /** * Gets the server. * @return the server */ public CogdaLiveDataServer getServer() { return _server; } /** * Gets the fudgeContext. * @return the fudgeContext */ public FudgeContext getFudgeContext() { return _fudgeContext; } /** * Gets the messageSender. * @return the messageSender */ public FudgeMessageSender getMessageSender() { return _messageSender; } /** * Gets the user. * @return the user */ public UserPrincipal getUserPrincipal() { return _userPrincipal; } /** * Gets the user. * @return the user */ public UserAccount getUser() { return _user; } /** * Sets the user. * @param user the user */ public void setUser(UserAccount user) { _user = user; } @Override public void connectionReset(FudgeConnection connection) { s_logger.warn("Connection Reset"); } @Override public void connectionFailed(FudgeConnection connection, Exception cause) { // TODO kirk 2012-08-15 -- Fix this so that failed connections result in // torn down client connections. // Cause may be null. s_logger.warn("Connection failed \"{}\"", (cause != null) ? cause.getMessage() : "no cause"); s_logger.info("Connection failed", cause); getServer().removeClient(this); } public void handshakeMessage(FudgeContext fudgeContext, FudgeMsgEnvelope msgEnvelope) { // REVIEW kirk 2012-07-23 -- If there are multiple versions of the protocol have to check // the schema on the envelope. FudgeMsg msg = msgEnvelope.getMessage(); if (CogdaMessageType.getFromMessage(msg) != CogdaMessageType.CONNECTION_REQUEST) { // On failure tear down the connection when http://jira.opengamma.com/browse/PLAT-2458 is done. throw new OpenGammaRuntimeException("Cannot handle any other message than connection request as first message in COGDA protocol."); } ConnectionRequestMessage request = ConnectionRequestBuilder.buildObjectStatic(new FudgeDeserializer(fudgeContext), msg); // Wrap this in synchronized to force the cache flush. synchronized (this) { _userPrincipal = getServer().authenticate(request.getUserName(), request.getPassword()); if (_userPrincipal != null) { _user = getServer().getUserAccount(request.getUserName()); } } if (getUserPrincipal() == null) { ConnectionResponseMessage response = new ConnectionResponseMessage(); response.setResult(ConnectionResult.NOT_AUTHORIZED); sendMessage(ConnectionResponseBuilder.buildMessageStatic(new FudgeSerializer(fudgeContext), response)); // On failure tear down the connection when http://jira.opengamma.com/browse/PLAT-2458 is done. getServer().removeClient(this); } else { ConnectionResponseMessage response = new ConnectionResponseMessage(); response.setResult(ConnectionResult.NEW_CONNECTION_SUCCESS); response.setAvailableServers(getServer().getAvailableServers()); response.applyCapabilities(getServer().getCapabilities()); sendMessage(ConnectionResponseBuilder.buildMessageStatic(new FudgeSerializer(fudgeContext), response)); } } @Override public void messageReceived(FudgeContext fudgeContext, FudgeMsgEnvelope msgEnvelope) { if (getUserPrincipal() == null) { throw new OpenGammaRuntimeException("Cannot operate, failed user authentication."); } FudgeMsg msg = msgEnvelope.getMessage(); CogdaLiveDataCommandResponseMessage response = null; switch (CogdaMessageType.getFromMessage(msg)) { case SNAPSHOT_REQUEST: response = handleSnapshotRequest(fudgeContext, msg); break; case SUBSCRIPTION_REQUEST: response = handleSubscriptionRequest(fudgeContext, msg); break; case UNSUBSCRIBE: handleUnsubscription(fudgeContext, msg); break; default: // Illegal here. // Need an "ILLEGAL_COMMAND" message. break; } if (response != null) { sendMessage(CogdaLiveDataBuilderUtil.buildCommandResponseMessage(fudgeContext, response)); } } protected boolean isEntitled(String operation, ExternalId subscriptionId, String normalizationScheme) { return true; // TODO: check permissions against user // String entitlementDetail = MessageFormat.format("/{0}/{1}[{2}]", subscriptionId.getScheme(), subscriptionId.getValue(), normalizationScheme); // String entitlementString = EntitlementUtils.generateEntitlementString(true, operation, "cogda", entitlementDetail); // return EntitlementUtils.userHasEntitlement(getUser(), entitlementString); } /** * @param fudgeContext * @param msg */ private CogdaLiveDataCommandResponseMessage handleSnapshotRequest(FudgeContext fudgeContext, FudgeMsg msg) { CogdaLiveDataSnapshotRequestMessage request = CogdaLiveDataSnapshotRequestBuilder.buildObjectStatic(new FudgeDeserializer(fudgeContext), msg); CogdaLiveDataSnapshotResponseMessage response = new CogdaLiveDataSnapshotResponseMessage(); response.setCorrelationId(request.getCorrelationId()); response.setSubscriptionId(request.getSubscriptionId()); response.setNormalizationScheme(request.getNormalizationScheme()); if (!getServer().isValidLiveData(request.getSubscriptionId(), request.getNormalizationScheme())) { response.setGenericResult(CogdaCommandResponseResult.NOT_AVAILABLE); } else if (!isEntitled(SNAPSHOT, request.getSubscriptionId(), request.getNormalizationScheme())) { response.setGenericResult(CogdaCommandResponseResult.NOT_AUTHORIZED); } else { LastKnownValueStore lkvStore = getServer().getLastKnownValueStore(request.getSubscriptionId(), request.getNormalizationScheme()); FudgeMsg fields = null; if (lkvStore != null) { fields = lkvStore.getFields(); } else { s_logger.warn("Valid live data {} lacks fields in LKV store", request); fields = fudgeContext.newMessage(); } response.setGenericResult(CogdaCommandResponseResult.SUCCESSFUL); response.setValues(fields); } return response; } /** * @param fudgeContext * @param msg */ private CogdaLiveDataCommandResponseMessage handleSubscriptionRequest(FudgeContext fudgeContext, FudgeMsg msg) { CogdaLiveDataSubscriptionRequestMessage request = CogdaLiveDataSubscriptionRequestBuilder.buildObjectStatic(new FudgeDeserializer(fudgeContext), msg); CogdaLiveDataSubscriptionResponseMessage response = new CogdaLiveDataSubscriptionResponseMessage(); response.setCorrelationId(request.getCorrelationId()); response.setSubscriptionId(request.getSubscriptionId()); response.setNormalizationScheme(request.getNormalizationScheme()); // TODO kirk 2012-07-23 -- Check entitlements. if (!getServer().isValidLiveData(request.getSubscriptionId(), request.getNormalizationScheme())) { response.setGenericResult(CogdaCommandResponseResult.NOT_AVAILABLE); } else if (!isEntitled(SUBSCRIBE, request.getSubscriptionId(), request.getNormalizationScheme())) { response.setGenericResult(CogdaCommandResponseResult.NOT_AUTHORIZED); } else { LastKnownValueStore lkvStore = getServer().getLastKnownValueStore(request.getSubscriptionId(), request.getNormalizationScheme()); FudgeMsg fields = null; if (lkvStore != null) { fields = lkvStore.getFields(); } else { s_logger.warn("Valid live data {} lacks fields in LKV store", request); fields = fudgeContext.newMessage(); } response.setGenericResult(CogdaCommandResponseResult.SUCCESSFUL); response.setSnapshot(fields); _subscriptions.putIfAbsent(new LiveDataSpecification(request.getNormalizationScheme(), request.getSubscriptionId()), Boolean.TRUE); } return response; } private void handleUnsubscription(FudgeContext fudgeContext, FudgeMsg msg) { CogdaLiveDataUnsubscribeMessage request = CogdaLiveDataUnsubscribeBuilder.buildObjectStatic(new FudgeDeserializer(fudgeContext), msg); _subscriptions.remove(new LiveDataSpecification(request.getNormalizationScheme(), request.getSubscriptionId())); } private void sendMessage(FudgeMsg msg) { _writerLock.lock(); try { getMessageSender().send(msg); } finally { _writerLock.unlock(); } } public boolean liveDataReceived(LiveDataValueUpdate valueUpdate) { if (!_subscriptions.containsKey(valueUpdate.getSpecification())) { return false; } _valuesToSendLock.lock(); try { _valuesToSend.put(valueUpdate.getSpecification(), valueUpdate.getFields()); } finally { _valuesToSendLock.unlock(); } return true; } public void sendAllUpdates() { _writerLock.lock(); try { _valuesToSendLock.lock(); try { for (Map.Entry<LiveDataSpecification, FudgeMsg> entry : _valuesToSend.entrySet()) { sendValueUpdate(entry.getKey(), entry.getValue()); } _valuesToSend.clear(); } finally { _valuesToSendLock.unlock(); } } finally { _writerLock.unlock(); } } /** * @param key * @param values */ private void sendValueUpdate(LiveDataSpecification key, FudgeMsg values) { CogdaLiveDataUpdateMessage message = new CogdaLiveDataUpdateMessage(); // REVIEW kirk 2012-07-23 -- This is a terrible terrible idea performance wise, this next line. message.setSubscriptionId(key.getIdentifiers().getExternalIds().iterator().next()); message.setNormalizationScheme(key.getNormalizationRuleSetId()); message.setValues(values); FudgeMsg msg = CogdaLiveDataUpdateBuilder.buildMessageStatic(new FudgeSerializer(getFudgeContext()), message); try { getMessageSender().send(msg); } catch (Exception e) { s_logger.info("Exception thrown; assuming socket closed and tearing down client."); // Note that the actual connection state will be handled by the FudgeConnectionStateListener callback. } } }