/* * Copyright (C) 2012 - present by Yann Le Tallec. * Please see distribution for license. */ package com.assylias.jbloomberg; import static com.assylias.jbloomberg.AbstractResultParser.SecurityDataElements.FIELD_DATA; import static com.assylias.jbloomberg.AbstractResultParser.SecurityDataElements.FIELD_EXCEPTIONS; import static com.assylias.jbloomberg.AbstractResultParser.SecurityDataElements.SECURITY_ERROR; import com.bloomberglp.blpapi.Element; import com.bloomberglp.blpapi.InvalidConversionException; import com.bloomberglp.blpapi.InvalidRequestException; import com.bloomberglp.blpapi.Message; import com.bloomberglp.blpapi.Name; import com.bloomberglp.blpapi.NotFoundException; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Locale; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base class to parse results from requests. * This implementation is thread safe as the Bloomberg API might send results through more than one thread. */ abstract class AbstractResultParser<T extends AbstractRequestResult> implements ResultParser<T> { private static final Logger logger = LoggerFactory.getLogger(AbstractResultParser.class); protected final static DateTimeFormatter BB_RESULT_DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; //'2011-12-03' protected final static DateTimeFormatter BB_RESULT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); /** * lock used to create the result object only once */ private final Object lock = new Object(); /** * List of received messages - must be thread safe */ private final Collection<Message> messages = new ConcurrentLinkedQueue<>(); /** * boolean used to make sure noMoreMessages is only called once - initially false. */ private final AtomicBoolean noMoreMessagesHasRun = new AtomicBoolean(); /** * Whether additional messages should be expected or not. */ private final CountDownLatch noMoreMessages = new CountDownLatch(1); /** * The result of the parsing operation - guarded by lock */ private T result; @Override public void addMessage(Message msg) { if (noMoreMessagesHasRun.get()) { throw new IllegalStateException("Can't add messages once noMoreMessages has been called"); } messages.add(msg); } @Override public void noMoreMessages() { if (!noMoreMessagesHasRun.compareAndSet(false, true)) { throw new IllegalStateException("This method should not be called more than once"); } noMoreMessages.countDown(); } @Override public T getResult() throws InterruptedException { noMoreMessages.await(); setResultIfNull(); return result; } @Override public T getResult(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException { if (!noMoreMessages.await(timeout, unit)) { throw new TimeoutException("Could not compute the result within " + timeout + " " + unit.toString().toLowerCase(Locale.ENGLISH)); } setResultIfNull(); return result; } private void setResultIfNull() { synchronized(lock) { if (result == null) { result = getRequestResult(); parse(messages); } } } /** * Some shared element names */ protected static final Name ERROR_INFO = new Name("errorInfo"); protected static final Name SECURITY_DATA = new Name("securityData"); protected static final Name SECURITY = new Name("security"); protected static final Name DATE = new Name("date"); protected static enum SecurityDataElements { SECURITY("security"), SEQUENCE_NUMBER("sequenceNumber"), FIELD_DATA("fieldData"), FIELD_EXCEPTIONS("fieldExceptions"), SECURITY_ERROR("securityError"); private final Name elementName; private SecurityDataElements(String elementName) { this.elementName = new Name(elementName); } protected Name asName() { return elementName; } } private static final Name RESPONSE_ERROR = new Name("responseError"); private static enum ErrorInfoElements { SOURCE("source"), CODE("code"), CATEGORY("category"), MESSAGE("message"), SUB_CATEGORY("subcategory"); private final Name elementName; private ErrorInfoElements(String elementName) { this.elementName = new Name(elementName); } private Name asName() { return elementName; } } protected abstract T getRequestResult(); private void parse(Collection<Message> messages) { for (Message msg : messages) { Element response = msg.asElement(); parseResponse(response); } } private void parseResponse(Element response) { if (response.hasElement(RESPONSE_ERROR, true)) { Element errorInfo = response.getElement(RESPONSE_ERROR); logger.info("Response error: {}", errorInfo); String errorMessage = parseErrorInfo(errorInfo); if (isSecurityError(errorInfo)) { //Normally only happen for an IntradayBarRequest or IntradayTickRequest result.addSecurityError(errorMessage); } else { throw new InvalidRequestException("ResponseError received (generally caused by malformed request or service down): " + errorMessage); } } parseResponseNoResponseError(response); } /** * @return a string representation of the error */ protected String parseErrorInfo(Element errorInfo) { try { return errorInfo.getElementAsString(ErrorInfoElements.MESSAGE.asName()); } catch (NotFoundException | InvalidConversionException e) { return "Could not parse the errorInfo element: " + errorInfo; } } /** * * @param errorInfo an errorInfo element * * @return true if this error is due to an invalid security */ private boolean isSecurityError(Element errorInfo) { return errorInfo.hasElement(ErrorInfoElements.CATEGORY.asName()) && errorInfo.getElementAsString(ErrorInfoElements.CATEGORY.asName()).equals("BAD_SEC"); } /** * Trying to use the most specific primitive type. * Primitives will get auto-boxed. */ protected void addField(LocalDate date, String security, Element field) { String fieldName = field.name().toString(); Object value = BloombergUtils.getSpecificObjectOf(field); result.add(date, security, fieldName, value); } protected void addField(OffsetDateTime date, Element field) { String fieldName = field.name().toString(); Object value = BloombergUtils.getSpecificObjectOf(field); result.add(date, fieldName, value); } protected void addField(String security, Element field) { String fieldName = field.name().toString(); Object value = BloombergUtils.getSpecificObjectOf(field); result.add(security, fieldName, value); } protected void addSecurityError(String security) { result.addSecurityError(security); } protected void addFieldError(String field) { result.addFieldError(field); } /** * This method must parse the valid part of the response (typically, the securityData or barData Element * * @param response The whole response element, including the responseError element if any (in which case it was * empty). */ protected abstract void parseResponseNoResponseError(Element response); protected void parseSecurityData(Element securityData) { String security = securityData.getElementAsString(SECURITY); if (securityData.hasElement(SECURITY_ERROR.asName(), true)) { Element errorInfo = securityData.getElement(SECURITY_ERROR.asName()); logger.info("Security error on {}: {}", security, errorInfo); addSecurityError(security); } else if (securityData.hasElement(FIELD_EXCEPTIONS.asName(), true)) { Element fieldExceptionsArray = securityData.getElement(FIELD_EXCEPTIONS.asName()); parseFieldExceptionsArray(fieldExceptionsArray); } if (securityData.hasElement(FIELD_DATA.asName(), true)) { Element fieldDataArray = securityData.getElement(FIELD_DATA.asName()); parseFieldDataArray(security, fieldDataArray); } } /** * Adds the field exceptions to the MultipleRequestResult object. Assumes that one field can't generate more than * one * exception. * In other words, we assume that if there are several exceptions, each corresponds to a different field. */ protected void parseFieldExceptionsArray(Element fieldExceptionsArray) { for (int i = 0; i < fieldExceptionsArray.numValues(); i++) { Element fieldException = fieldExceptionsArray.getValueAsElement(i); String field = fieldException.getElementAsString("fieldId"); Element errorInfo = fieldException.getElement(ERROR_INFO); logger.info("Field exception on {}: {}", field, errorInfo); addFieldError(field); } } protected void parseFieldDataArray(String security, Element fieldDataArray) { //does nothing here - needs to be implemented by subclasses if required } }