/* * Copyright (C) 2012 - present by Yann Le Tallec. * Please see distribution for license. */ package com.assylias.jbloomberg; import static com.assylias.jbloomberg.BloombergEventHandler.BloombergConnectionState.SESSION_STARTED; import static com.assylias.jbloomberg.BloombergEventHandler.BloombergConnectionState.SESSION_STARTUP_FAILURE; import com.bloomberglp.blpapi.CorrelationID; import com.bloomberglp.blpapi.Element; import com.bloomberglp.blpapi.Event; import com.bloomberglp.blpapi.EventHandler; import com.bloomberglp.blpapi.Message; import com.bloomberglp.blpapi.Name; import com.bloomberglp.blpapi.Session; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import static java.util.Objects.requireNonNull; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An implementation of EventHandler. This is where all the messages are received from the Bloomberg session and * forwarded to the relevant parsers for requests or to a queue for subscriptions. * The typical lifecycle is as follows: * <ul> * <li> SessionConnectionUp </li> * <li> SessionStarted </li> * <li> SessionConnectionDown </li> * <li> SessionTerminated </li> * </ul> * A SessionConnectionDown will be sent if the terminal gets logged out by another device - if setAutoRestartOnDisconnection has been set to true, it will be * followed by a SessionConnectionUp although no data will be coming until the user logs in again. If setAutoRestartOnDisconnection has been set to false (default) * it will be followed by a SessionTerminated signal. */ final class BloombergEventHandler implements EventHandler { private final static Logger logger = LoggerFactory.getLogger(BloombergEventHandler.class); private final BlockingQueue<Data> subscriptionDataQueue; private final Consumer<SessionState> stateListener; private final Map<CorrelationID, ResultParser> parsers = new ConcurrentHashMap<>(); private volatile Runnable runOnSessionStarted; private volatile Consumer<BloombergException> runOnSessionStartupFailure; /** * * @param subscriptionDataQueue the queue to which subscription data will be posted. * @param stateListener a listener that will be called on each new SESSION_STATUS event. * * @throws NullPointerException if any of the arguments are null. */ public BloombergEventHandler(BlockingQueue<Data> subscriptionDataQueue, Consumer<SessionState> stateListener) { this.subscriptionDataQueue = requireNonNull(subscriptionDataQueue); this.stateListener = requireNonNull(stateListener); } @Override public void processEvent(Event event, Session session) { try { EventTypeEnum type = EventTypeEnum.get(event); switch (type) { case SESSION_STATUS: for (Message msg : event) { logger.debug("[{}] {}", type, msg); BloombergConnectionState state = BloombergConnectionState.get(msg.messageType()); if (state == SESSION_STARTED) runOnSessionStarted.run(); if (state == SESSION_STARTUP_FAILURE) runOnSessionStartupFailure.accept(new BloombergException(msg.toString())); if (state != null) stateListener.accept(SessionState.from(state)); } break; case PARTIAL_RESPONSE: for (Message msg : event) { logger.trace("[{}] {}", type, msg); CorrelationID cId = msg.correlationID(); ResultParser parser = parsers.get(cId); if (parser != null) { parser.addMessage(msg); } } break; case RESPONSE: Set<CorrelationID> endOfTransmission = new HashSet<>(); for (Message msg : event) { logger.trace("[{}] {}", type, msg); CorrelationID cId = msg.correlationID(); ResultParser parser = parsers.get(cId); if (parser != null) { endOfTransmission.add(cId); parser.addMessage(msg); } } for (CorrelationID cId : endOfTransmission) { ResultParser parser = parsers.remove(cId); //remove from the map - not needed any longer. parser.noMoreMessages(); } break; case SUBSCRIPTION_DATA: for (Message msg : event) { CorrelationID id = msg.correlationID(); int numFields = msg.asElement().numElements(); for (int i = 0; i < numFields; ++i) { Element field = msg.asElement().getElement(i); if (!field.isNull()) { Data data = new Data(id, field.name().toString(), BloombergUtils.getSpecificObjectOf(field)); try { subscriptionDataQueue.put(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; //ignore the rest } logger.trace("[SUBS_DATA] {}", data); } } } break; case SUBSCRIPTION_STATUS: for (Message msg : event) { CorrelationID id = msg.correlationID(); logger.debug("[{}] id=[{}] {}", type, id, msg); String msgType = msg.messageType().toString(); if (!"SubscriptionStarted ".equals(msgType)) { logger.debug("[{}] id=[{}] {}", type, id, msg); Element msgElement = msg.asElement(); Data data = null; if (msgElement.hasElement("reason")){ Element reason = msg.asElement().getElement("reason"); if (reason.hasElement("errorCode") && reason.hasElement("category") && reason.hasElement("description")) { SubscriptionError e = new SubscriptionError(msgType, msg.topicName(), reason.getElementAsInt32("errorCode"), reason.getElementAsString("category"), reason.getElementAsString("description")); data = new Data(id, "", e); } } if (data == null) data = new Data(id, "", new SubscriptionError(msgType, msg.topicName(), 0, "", msg.toString())); try { subscriptionDataQueue.put(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; //ignore the rest } } } break; default: for (Message msg : event) { CorrelationID id = msg.correlationID(); logger.debug("[{}] id=[{}] {}", type, id, msg); } } //catch all - this code will run in one of the Bloomberg API's threads so we have no way to catch exceptions otherwise } catch (Exception e) { logger.error("Error in Bloomberg EventHandler: " + e.getMessage(), e); throw e; //rethrow in case the Bloomberg API needs to see it } } /** * * @param runOnSessionStarted this runnable will be run as soon as the session is started */ void onSessionStarted(Runnable runOnSessionStarted) { this.runOnSessionStarted = runOnSessionStarted; } /** * * @param runnable a runnable to run if the session startup process fails */ void onSessionStartupFailure(Consumer<BloombergException> runOnSessionStartupFailure) { this.runOnSessionStartupFailure = runOnSessionStartupFailure; } void setParser(CorrelationID requestId, ResultParser parser) { parsers.put(requestId, parser); } /** * Wrapping all the EventType objects in an enum for easier use */ static enum EventTypeEnum { ADMIN(Event.EventType.ADMIN), AUTHORIZATION_STATUS(Event.EventType.AUTHORIZATION_STATUS), PARTIAL_RESPONSE(Event.EventType.PARTIAL_RESPONSE), REQUEST(Event.EventType.REQUEST), REQUEST_STATUS(Event.EventType.REQUEST_STATUS), RESOLUTION_STATUS(Event.EventType.RESOLUTION_STATUS), RESPONSE(Event.EventType.RESPONSE), SERVICE_STATUS(Event.EventType.SERVICE_STATUS), SESSION_STATUS(Event.EventType.SESSION_STATUS), SUBSCRIPTION_DATA(Event.EventType.SUBSCRIPTION_DATA), SUBSCRIPTION_STATUS(Event.EventType.SUBSCRIPTION_STATUS), TIMEOUT(Event.EventType.TIMEOUT), TOKEN_STATUS(Event.EventType.TOKEN_STATUS), TOPIC_STATUS(Event.EventType.TOPIC_STATUS); private final static Map<Event.EventType, EventTypeEnum> map = new HashMap<>(EventTypeEnum.values().length, 1); static { for (EventTypeEnum e : values()) { map.put(e.evtType, e); } } private final Event.EventType evtType; private EventTypeEnum(Event.EventType evtType) { this.evtType = evtType; } static EventTypeEnum get(Event evt) { return map.get(evt.eventType()); } } /** * BloombergConnectionState is an enum representing the possible states of the underlying Bloomberg connection. The difference with the SessionState enum * is that it only contains states sent by the Bloomberg connection. */ static enum BloombergConnectionState { SESSION_STARTED("SessionStarted"), SESSION_STARTUP_FAILURE("SessionStartupFailure"), SESSION_CONNECTION_DOWN("SessionConnectionDown"), SESSION_CONNECTION_UP("SessionConnectionUp"), SESSION_TERMINATED("SessionTerminated"); private final static Map<Name, BloombergConnectionState> map = new HashMap<>(BloombergConnectionState.values().length, 1); static { for (BloombergConnectionState e : values()) map.put(e.name, e); } private final Name name; private BloombergConnectionState(String s) { this.name = new Name(s); } static BloombergConnectionState get(Name name) { BloombergConnectionState s = map.get(name); if (s == null) logger.info("Not a valid connection state: " + name); return s; } } }