public final class MetaDataResult {
  private static Logger s_logger = LoggerFactory.getLogger(MetaDataResult.class);
  private static final String COLUMN_NAMES_FIELD = "column_names";
  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE;
  private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
  private static final String DATA_SOURCE_FIELD = "database_code";
  private static final String CODE_FIELD = "dataset_code";
  private JSONObject _jsonObject;

  private MetaDataResult(final JSONObject jsonObject) {
    _jsonObject = jsonObject;
  }

  /**
   * Factory method for creating and instance of a MetaDataResult.
   * 
   * @param jsonObject the JSON object returned by Quandl, not null
   * @return a MetaDataResult instance, not null
   */
  public static MetaDataResult of(final JSONObject jsonObject) {
    ArgumentChecker.notNull(jsonObject, "jsonObject");
    return new MetaDataResult(jsonObject);
  }

  /**
   * Extract a HeaderDefinition from the meta data. Throws a QuandlRuntimeException if it cannot construct a valid HeaderDefinition
   * 
   * @return the header definition, not null
   */
  public HeaderDefinition getHeaderDefinition() {
    JSONArray jsonArray = null;
    try {
      jsonArray = _jsonObject.getJSONArray(COLUMN_NAMES_FIELD);
      List<String> columnNames = new ArrayList<String>(jsonArray.length());
      for (int i = 0; i < jsonArray.length(); i++) {
        columnNames.add(jsonArray.getString(i));
      }
      return HeaderDefinition.of(columnNames);
    } catch (JSONException ex) {
      s_logger.error("Metadata had unexpected structure - could not extract column_names field. Was:\n{}", _jsonObject.toString());
      throw new QuandlRuntimeException("Metadata had unexpected structure", ex);
    }
  }

  /**
   * Get the Quandl code associated with this metadata.
   * 
   * @return the quandl code (DATASOURCE/CODE form), null if not present.
   */
  public String getQuandlCode() {
    try {
      String dataSourceField = getString(DATA_SOURCE_FIELD);
      String codeField = getString(CODE_FIELD);
      // if either not present, will throw an exception.
      return dataSourceField + "/" + codeField;
    } catch (RuntimeException ex) {
      return null;
    }
  }

  /**
   * Get a String field. Throws a QuandlRuntimeException if it cannot find the field
   * 
   * @param fieldName the name of the field
   * @return the field value, or null if the field is null
   */
  public String getString(final String fieldName) {
    try {
      return _jsonObject.getString(fieldName);
    } catch (JSONException ex) {
      throw new RuntimeException("Cannot find field", ex);
    }
  }

  /**
   * Get a LocalDate field (converted from a String internally). Throws a QuandlRuntimeException if it cannot find the field
   * 
   * @param fieldName the name of the field
   * @return the field value, or null if the field is null
   */
  public LocalDate getLocalDate(final String fieldName) {
    try {
      if (_jsonObject.isNull(fieldName)) {
        return null;
      } else {
        return LocalDate.parse(_jsonObject.getString(fieldName), DATE_FORMATTER);
      }
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Cannot find field", ex);
    }
  }

  /**
   * Get a LocalDate field (converted from a String internally). Throws a QuandlRuntimeException if it cannot find the field
   * 
   * @param fieldName the name of the field
   * @return the field value, or null if the field is null
   */
  public OffsetDateTime getOffsetDateTime(final String fieldName) {
    try {
      if (_jsonObject.isNull(fieldName)) {
        return null;
      } else {
        return OffsetDateTime.parse(_jsonObject.getString(fieldName), DATE_TIME_FORMATTER);
      }
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Cannot find field", ex);
    }
  }

  /**
   * Return whether a field is present.
   * 
   * @param fieldName the name of the field
   * @return true, if the field is present
   */
  public boolean has(final String fieldName) {
    return _jsonObject.has(fieldName);
  }

  /**
   * Get a Double field. This attempts to work around the stupid NaN is null behavior by explicitly testing for null. Throws a
   * QuandlRuntimeException if it cannot find the field
   * 
   * @param fieldName the name of the field
   * @return the field value, or null if the field is null
   */
  public Double getDouble(final String fieldName) {
    try {
      if (_jsonObject.isNull(fieldName)) {
        return null;
      } else {
        return _jsonObject.getDouble(fieldName);
      }
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Cannot find field", ex);
    }
  }

  /**
   * An iterator over the string field names.
   * 
   * @return the iterator
   */
  public Iterator<String> iterator() {
    return  _jsonObject.keys();
  }

  /**
   * Get the underlying JSON object, useful if the data structure has changed since release.
   * 
   * @return the underlying JSON object
   */
  public JSONObject getRawJSON() {
    return _jsonObject;
  }

  /**
   * Use the underlying JSON toString() to show full data structure. This means data can be seen even if it isn't in a flat structure. To
   * get a pretty printed version, use toPrettyPrintedString()
   * 
   * @return a string representation of the meta-data laid out as a JSON message (single line).
   */
  @Override
  public String toString() {
    return _jsonObject.toString();
  }

  /**
   * Print a nicely formatted representation of this object. Currently prints a nicely formatted JSON representation.
   * 
   * @return a string containing a multi-line description of this object
   */
  public String toPrettyPrintedString() {
    return PrettyPrinter.toPrettyPrintedString(this);
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof MetaDataResult)) {
      return false;
    }
    final MetaDataResult other = (MetaDataResult) obj;
    // This is a work-around because JSONObject doesn't override equals().
    // it will be quite expensive, but should suffice.
    return getRawJSON().toString().equals(other.getRawJSON().toString());
  }

  @Override
  public int hashCode() {
    // This is a work-around because JSONObject doesn't override hashCode().
    // it will be quite expensive, but should suffice.
    return getRawJSON().toString().hashCode();
  }
} public final class SearchResult {
  private static Logger s_logger = LoggerFactory.getLogger(SearchResult.class);
  private static final String META_OBJECT_FIELD = "meta";
  private static final String DATASETS_ARRAY_FIELD = "datasets";
  private JSONObject _jsonObject;

  private SearchResult(final JSONObject jsonObject) {
    _jsonObject = jsonObject;
  }

  /**
   * Factory method for creating and instance of a MetaDataResult.
   * 
   * @param jsonObject the JSON object returned by Quandl, not null
   * @return a MetaDataResult instance, not null
   */
  public static SearchResult of(final JSONObject jsonObject) {
    ArgumentChecker.notNull(jsonObject, "jsonObject");
    return new SearchResult(jsonObject);
  }

  /**
   * Get the total number of documents in this result set. Throws a QuandlRuntimeException if Quandl response doesn't contain this field,
   * although it should.
   * 
   * @return the total number of documents in this result set
   */
  public int getTotalDocuments() {
    try {
      final int totalDocs = _jsonObject.getJSONObject(META_OBJECT_FIELD).getInt("total_count");
      return totalDocs;
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Could not find total_count field in results from Quandl", ex);
    }
  }

  /**
   * Get the number of documents per page in this result set. Throws a QuandlRuntimeException if Quandl response doesn't contain this field,
   * although it should.
   * 
   * @return the number of documents per page in this result set
   */
  public int getDocumentsPerPage() {
    try {
      final int perPage = _jsonObject.getJSONObject(META_OBJECT_FIELD).getInt("per_page");
      return perPage;
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Could not find total_count field in results from Quandl", ex);
    }
  }

  /**
   * Get the current page of this result set. You can specify the page number in the request. Throws a QuandlRuntimeException if Quandl
   * response doesn't contain this field, although it should.
   * 
   * @return the page number of this result set
   */
  public int getCurrentPage() {
    try {
      final int currentPage = _jsonObject.getJSONObject(META_OBJECT_FIELD).getInt("current_page");
      return currentPage;
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Could not find total_count field in results from Quandl", ex);
    }
  }

  /**
   * Extract a list of MetaDataResult objects, each one representing a match. Throws a QuandlRuntimeException if it cannot construct a valid
   * HeaderDefinition
   * 
   * @return the header definition, not null
   */
  public List<MetaDataResult> getMetaDataResultList() {
    JSONArray jsonArray = null;
    try {
      jsonArray = _jsonObject.getJSONArray(DATASETS_ARRAY_FIELD);
      List<MetaDataResult> metaDataResults = new ArrayList<MetaDataResult>(jsonArray.length());
      for (int i = 0; i < jsonArray.length(); i++) {
        metaDataResults.add(MetaDataResult.of(jsonArray.getJSONObject(i)));
      }
      return metaDataResults;
    } catch (JSONException ex) {
      s_logger.error("Metadata had unexpected structure - could not extract datasets field, was:\n{}", _jsonObject.toString());
      throw new QuandlRuntimeException("Metadata had unexpected structure", ex);
    }
  }

  /**
   * An iterator over the string field names.
   * 
   * @return the iterator
   */
  public Iterator<MetaDataResult> iterator() {
    return getMetaDataResultList().iterator();
  }

  /**
   * Get the underlying JSON object, useful if the data structure has changed since release.
   * 
   * @return the underlying JSON object
   */
  public JSONObject getRawJSON() {
    return _jsonObject;
  }

  /**
   * Use the underlying JSON toString() to show full data structure. This means data can be seen even if it isn't in a flat structure. To
   * get a pretty printed version, use getRawJSON().toString(indent)
   * 
   * @return a string representation of the meta-data laid out as a JSON message (single line).
   */
  @Override
  public String toString() {
    return _jsonObject.toString();
  }

  /**
   * Print a nicely formatted representation of this object. Currently prints a nicely formatted JSON representation.
   * 
   * @return a string containing a multi-line description of this object
   */
  public String toPrettyPrintedString() {
    return PrettyPrinter.toPrettyPrintedString(this);
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof SearchResult)) {
      return false;
    }
    final SearchResult other = (SearchResult) obj;
    // This is a work-around because JSONObject doesn't override equals().
    // it will be quite expensive, but should suffice.
    return getRawJSON().toString().equals(other.getRawJSON().toString());
  }

  @Override
  public int hashCode() {
    // This is a work-around because JSONObject doesn't override hashCode().
    // it will be quite expensive, but should suffice.
    return getRawJSON().toString().hashCode();
  }
} @SuppressWarnings("deprecation")
public final class SessionOptions {
  private String _authToken;
  private RESTDataProvider _restDataProvider;
  private boolean _isRestDataProviderSet;
  private RetryPolicy _retryPolicy;

  private SessionOptions(final Builder builder) {
    _authToken = builder._authToken;
    _restDataProvider = builder._restDataProvider;
    _retryPolicy = builder._retryPolicy;
    _isRestDataProviderSet = builder._isRestDataProviderSet;
  }

  /**
   * Internal Builder class.
   */
  public static final class Builder {
    private static final long ONE_SECOND = 1000L;
    private static final long FIVE_SECONDS = 5000L;
    private static final long TWENTY_SECONDS = 20000L;
    private static final long SIXTY_SECONDS = 60000L;

    private String _authToken;
    private RESTDataProvider _restDataProvider = new DefaultRESTDataProvider();
    private boolean _isRestDataProviderSet;
    private RetryPolicy _retryPolicy = RetryPolicy
        .createSequenceRetryPolicy(new long[] { ONE_SECOND, FIVE_SECONDS, TWENTY_SECONDS, SIXTY_SECONDS });

    private Builder(final String authToken) {
      _authToken = authToken;
    }

    /**
     * Specify a specific auth token.
     * 
     * @param authToken your auth token
     * @return a Builder object for chaining, call build() to complete
     */
    public static Builder withAuthToken(final String authToken) {
      ArgumentChecker.notNull(authToken, "authToken");
      return new Builder(authToken);
    }

    /**
     * Specify no auth token.
     * 
     * @return a Builder object for chaining, call build() to complete
     */
    public static Builder withoutAuthToken() {
      return new Builder(null);
    }

    /**
     * Specify a custom RESTDataProvider for the session to use when sending requests, intended for testing purposes.
     * 
     * @deprecated you should pass this into the QuandlSessionFactory instead
     * @param restDataProvider a RESTDataProvider for the session
     * @return this builder
     */
    public Builder withRESTDataProvider(final RESTDataProvider restDataProvider) {
      ArgumentChecker.notNull(restDataProvider, "restDataProvider");
      _restDataProvider = restDataProvider;
      _isRestDataProviderSet = true;
      return this;
    }

    /**
     * Specify the number of retries to execute before giving up on a request.
     * 
     * @param retryPolicy the policy to follow regarding retries
     * @return this builder
     */
    public Builder withRetryPolicy(final RetryPolicy retryPolicy) {
      ArgumentChecker.notNull(retryPolicy, "retryPolicy");
      _retryPolicy = retryPolicy;
      return this;
    }

    /**
     * Specify the length of time to wait before retrying
     */
    /**
     * Build an instance of QuandlOptions using the parameters in this builder instance.
     * 
     * @return an instance of QuandlOptions
     */
    public SessionOptions build() {
      return new SessionOptions(this);
    }
  }

  /**
   * Get the Quandl auth token stored in this SessionOptions, or null if none was specified.
   * 
   * @return the quandl API auth token String, or null if none was specified
   */
  public String getAuthToken() {
    return _authToken;
  }

  /**
   * @deprecated rest data provider now passed into session at creation. Get the REST data provider to use when sending API requests to
   *             Quandl.
   * @return the REST data provider, not null
   */
  public RESTDataProvider getRESTDataProvider() {
    return _restDataProvider;
  }

  /**
   * Was the REST data provider explicitly set? This allows us to preserve the behaviour of getRESTDataProvider in returning a default
   * implementation when not set.
   * 
   * @return true, if the REST data provider was explicitly set
   */
  public boolean isRESTDataProviderSet() {
    return _isRestDataProviderSet;
  }

  /**
   * Get the RetryPolicy to use in determining retry behaviour when calls to Quandl fail. The default is a four stage back-off of 1 second,
   * 5 seconds, 20 seconds and lastly 60 seconds.
   * 
   * @return the RetryPolicy
   */
  public RetryPolicy getRetryPolicy() {
    return _retryPolicy;
  }
} public class GenericQuandlSession<METADATA_TYPE, RAW_METADATA_TYPE, TABLE_TYPE, SEARCH_RESULT_TYPE>
    implements GenericQuandlSessionInterface<METADATA_TYPE, TABLE_TYPE, SEARCH_RESULT_TYPE> {
  private static final UriBuilder API_BASE_URL_V3 = UriBuilder.fromPath("https://www.quandl.com/api/v3");

  private static Logger s_logger = LoggerFactory.getLogger(GenericQuandlSession.class);

  private SessionOptions _sessionOptions;
  private GenericRESTDataProvider<RAW_METADATA_TYPE, TABLE_TYPE> _restDataProvider;
  private MetaDataPackager<METADATA_TYPE, RAW_METADATA_TYPE, SEARCH_RESULT_TYPE> _metaDataPackager;

  /**
   * The property name used to pass Quandl auth tokens into the JVM.
   */
  public static final String QUANDL_AUTH_TOKEN_PROPERTY_NAME = "quandl.auth.token";

  /**
   * Create a generic quandl session.
   * @param sessionOptions  the session options
   * @param restDataProvider  the rest data provider, which reads and encodes data
   * @param metaDataPackager  the meta data packager, which packages meta data
   */
  public GenericQuandlSession(final SessionOptions sessionOptions,
      final GenericRESTDataProvider<RAW_METADATA_TYPE, TABLE_TYPE> restDataProvider,
      final MetaDataPackager<METADATA_TYPE, RAW_METADATA_TYPE, SEARCH_RESULT_TYPE> metaDataPackager) {
    _sessionOptions = sessionOptions;
    _restDataProvider = restDataProvider;
    _metaDataPackager = metaDataPackager;
  }

  /**
   * Allow the client to be overridden by a test subclass.
   * 
   * @return a Jersey Client
   */
  protected Client getClient() {
    return ClientBuilder.newClient();
  }

  /**
   * Add authorization token to the web target.
   * 
   * @param target the web target
   */
  private WebTarget withAuthToken(final WebTarget target) {
    if (_sessionOptions.getAuthToken() != null) {
      return target.queryParam(AUTH_TOKEN_PARAM_NAME, _sessionOptions.getAuthToken());
    } else {
      return target;
    }
  }

  public TABLE_TYPE getDataSet(final String symbol) {
    return getDataSet(DataSetRequest.Builder.of(symbol).build());    
  }

  @Override
  public TABLE_TYPE getDataSet(final DataSetRequest request) {
    ArgumentChecker.notNull(request, "request");
    Client client = getClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = request.appendPathAndQueryParameters(target);
    target = withAuthToken(target);
    int retries = 0;
    QuandlRuntimeException lastException;
    do {
      try {
        return _restDataProvider.getTabularResponse(target, request);
      } catch (QuandlTooManyRequestsException qtmre) {
        lastException = qtmre;
        s_logger.debug("Quandl returned Too Many Requests, retrying if appropriate");
        if (qtmre.isDataExhausted()) {
          throw new QuandlRequestFailedException("Data request limit exceeded", qtmre);
        }
      } catch (QuandlServiceUnavailableException qsue) {
        lastException = qsue;
        s_logger.debug("Quandl returned Service Not Available, retrying if appropriate");
      }
    } while (_sessionOptions.getRetryPolicy().checkRetries(retries++));

    throw new QuandlRequestFailedException("Giving up on request after " + retries + " retries.", lastException);
  }

  @Override
  public METADATA_TYPE getMetaData(final MetaDataRequest request) {
    ArgumentChecker.notNull(request, "request");
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = request.appendPathAndQueryParameters(target);
    target = withAuthToken(target);
    int retries = 0;
    QuandlRuntimeException lastException;
    do {
      try {
        return _metaDataPackager.ofMetaData(_restDataProvider.getJSONResponse(target, request));
      } catch (QuandlTooManyRequestsException qtmre) {
        lastException = qtmre;
        s_logger.debug("Quandl returned Too Many Requests, retrying if appropriate");
        if (qtmre.isDataExhausted()) {
          throw new QuandlRequestFailedException("Data request limit exceeded", qtmre);
        }
      } catch (QuandlServiceUnavailableException qsue) {
        lastException = qsue;
        s_logger.debug("Quandl returned Service Not Available, retrying if appropriate");
      }
    } while (_sessionOptions.getRetryPolicy().checkRetries(retries++));

    throw new QuandlRequestFailedException("Giving up on request after " + retries + " retries.", lastException);
  }

  @Override
  public SEARCH_RESULT_TYPE search(final SearchRequest request) {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = request.appendPathAndQueryParameters(target);
    target = withAuthToken(target);
    int retries = 0;
    QuandlRuntimeException lastException;
    do {
      try {
        return _metaDataPackager.ofSearchResult(_restDataProvider.getJSONResponse(target, request));
      } catch (QuandlTooManyRequestsException qtmre) {
        lastException = qtmre;
        s_logger.debug("Quandl returned Too Many Requests, retrying if appropriate");
        if (qtmre.isDataExhausted()) {
          throw new QuandlRequestFailedException("Data request limit exceeded", qtmre);
        }
      } catch (QuandlServiceUnavailableException qsue) {
        lastException = qsue;
        s_logger.debug("Quandl returned Service Not Available, retrying if appropriate");
      }
    } while (_sessionOptions.getRetryPolicy().checkRetries(retries++));

    throw new QuandlRequestFailedException("Giving up on request after " + retries + " retries.", lastException);
  }
} public class ClassicMetaDataPackager implements MetaDataPackager<MetaDataResult, JSONObject, SearchResult> {
  /**
   * {@inheritDoc}
   */
  public MetaDataResult ofMetaData(final JSONObject rawData) {
    return MetaDataResult.of(rawData);
  }
  /**
   * {@inheritDoc}
   */
  public SearchResult ofSearchResult(final JSONObject rawData) {
    return SearchResult.of(rawData);
  }

} public interface GenericRESTDataProvider<RAW_METADATA_TYPE, TABLE_TYPE> {
  /**
   * HTTP Response code returned in some cases, usually when a request is nonsensical, e.g. requesting a non-existent column.
   */
  int UNPROCESSABLE_ENTITY = 422;
  /**
   * HTTP Response code returned when the client has made too many request - it is an indication of rate limiting on the server-side.
   */
  int TOO_MANY_REQUESTS = 429; // response code returned in some cases
  /**
   * HTTP Response code returned when the service is unavailable - it is an indication of a probably temporary error or load problem on the
   * server.
   */
  int SERVICE_UNAVAILABLE = 503;

  /**
   * Invoke a GET call on the web target and return the result as a parsed JSON object. Throws a QuandlRuntimeException if there was a CSV
   * parsing problem or response code was not OK
   * 
   * @param target  the WebTarget describing the call to make, not null
   * @param request  the request object or null
   * @return the parsed JSON object
   */
  RAW_METADATA_TYPE getJSONResponse(WebTarget target, Request request);

  /**
   * Invoke a GET call on the web target and return the result as a TabularResult (parsed CSV). Throws a QuandlRuntimeException if there was
   * a JSON parsing problem, network issue or response code was not OK
   * 
   * @param target  the WebTarget describing the call to make, not null
   * @param request  the request object or null
   * @return the parsed TabularResult
   */
  TABLE_TYPE getTabularResponse(WebTarget target, Request request);
} public interface MetaDataPackager<METADATA_TYPE, RAW_METADATA_TYPE, SEARCH_RESULT_TYPE> {
  /**
   * Package raw metadata in a query result wrapper.
   * @param rawData  the raw representation of the metadata
   * @return the wrapped query metadata
   */
  METADATA_TYPE ofMetaData(RAW_METADATA_TYPE rawData);

  /**
   * Package raw metadata into a search result wrapper.
   * @param rawData  the raw representation of the metadata
   * @return the wrapped search result metadata
   */
  SEARCH_RESULT_TYPE ofSearchResult(RAW_METADATA_TYPE rawData);
} public class JSONTableSawRESTDataProvider 
    extends AbstractRESTDataProvider<JSONObject, Table> 
    implements TableSawRESTDataProvider {
  private static final JSONObjectResponseProcessor JSON_OBJECT_RESPONSE_PROCESSOR = new JSONObjectResponseProcessor();
  private static final TableSawResponseProcessor TABLE_SAW_RESPONSE_PROCESSOR = new TableSawResponseProcessor();

  /**
   * Invoke a GET call on the web target and return the result as a parsed JSON object. Throws a QuandlUnprocessableEntityException if
   * Quandl returned a response code that indicates a nonsensical request Throws a QuandlTooManyRequestsException if Quandl returned a
   * response code indicating the client had made too many requests Throws a QuandlRuntimeException if there was a CSV parsing problem or
   * response code was unusual
   * 
   * @param target the WebTarget describing the call to make, not null
   * @return the parsed JSON object
   */
  public JSONObject getJSONResponse(final WebTarget target, final Request request) {
    return getResponse(target, JSON_OBJECT_RESPONSE_PROCESSOR, request);
  }

  /**
   * Invoke a GET call on the web target and return the result as a TabularResult (parsed CSV). Throws a QuandlUnprocessableEntityException
   * if Quandl returned a response code that indicates a nonsensical request Throws a QuandlTooManyRequestsException if Quandl returned a
   * response code indicating the client had made too many requests Throws a QuandlRuntimeException if there was a JSON parsing problem,
   * network issue or response code was unusual
   * 
   * @param target the WebTarget describing the call to make, not null
   * @return the parsed TabularResult
   */
  public Table getTabularResponse(final WebTarget target, Request request) {
    return getResponse(target, TABLE_SAW_RESPONSE_PROCESSOR, request);
  }
} public interface TableSawRESTDataProvider
    extends GenericRESTDataProvider<JSONObject, Table> {
  /**
   * Invoke a GET call on the web target and return the result as a parsed JSON object. Throws a QuandlUnprocessableEntityException if
   * Quandl returned a response code that indicates a nonsensical request Throws a QuandlTooManyRequestsException if Quandl returned a
   * response code indicating the client had made too many requests Throws a QuandlRuntimeException if there was a CSV parsing problem or
   * response code was unusual
   * 
   * @param target the WebTarget describing the call to make, not null
   * @param request the request object or null
   * @return the parsed JSON object
   */
  JSONObject getJSONResponse(WebTarget target, Request request);

  /**
   * Invoke a GET call on the web target and return the result as a TableSaw table. Throws a QuandlUnprocessableEntityException
   * if Quandl returned a response code that indicates a nonsensical request Throws a QuandlTooManyRequestsException if Quandl returned a
   * response code indicating the client had made too many requests Throws a QuandlRuntimeException if there was a JSON parsing problem,
   * network issue or response code was unusual
   * 
   * @param target the WebTarget describing the call to make, not null
   * @param request the request object or null
   * @return the resulting Table
   */
  Table getTabularResponse(WebTarget target, Request request);


} public final class ArgumentChecker {
  private static Logger s_logger = LoggerFactory.getLogger(ArgumentChecker.class);

  private ArgumentChecker() {
  }

  /**
   * Throws an exception if the argument is not null.
   * 
   * @param argument the object to check
   * @param name the name of the parameter
   */
  public static void notNull(final Object argument, final String name) {
    if (argument == null) {
      s_logger.error("Argument {} was null", name);
      throw new QuandlRuntimeException("Value " + name + " was null");
    }
  }

  /**
   * Throws an exception if the array argument is not null or empty.
   * 
   * @param <E> type of array
   * @param argument the object to check
   * @param name the name of the parameter
   */
  public static <E> void notNullOrEmpty(final E[] argument, final String name) {
    if (argument == null) {
      s_logger.error("Argument {} was null", name);
      throw new QuandlRuntimeException("Value " + name + " was null");
    } else if (argument.length == 0) {
      s_logger.error("Argument {} was empty array", name);
      throw new QuandlRuntimeException("Value " + name + " was empty array");
    }
  }

  /**
   * Throws an exception if the collection argument is not null or empty.
   * 
   * @param <E> type of array
   * @param argument the object to check
   * @param name the name of the parameter
   */
  public static <E> void notNullOrEmpty(final Collection<E> argument, final String name) {
    if (argument == null) {
      s_logger.error("Argument {} was null", name);
      throw new QuandlRuntimeException("Value " + name + " was null");
    } else if (argument.size() == 0) {
      s_logger.error("Argument {} was empty collection", name);
      throw new QuandlRuntimeException("Value " + name + " was empty collection");
    }
  }

  /**
   * Throws an exception if the string argument is not null or empty.
   * 
   * @param argument the String to check
   * @param name the name of the parameter
   */
  public static void notNullOrEmpty(final String argument, final String name) {
    if (argument == null) {
      s_logger.error("Argument {} was null", name);
      throw new QuandlRuntimeException("Value " + name + " was null");
    } else if (argument.length() == 0) {
      s_logger.error("Argument {} was empty string", name);
      throw new QuandlRuntimeException("Value " + name + " was empty string");
    }
  }
}
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jimmoores.quandl.MetaDataResult;
import com.jimmoores.quandl.SearchResult;
import com.jimmoores.quandl.SessionOptions;
import com.jimmoores.quandl.generic.GenericQuandlSession;
import com.jimmoores.quandl.processing.ClassicMetaDataPackager;
import com.jimmoores.quandl.processing.GenericRESTDataProvider;
import com.jimmoores.quandl.processing.MetaDataPackager;
import com.jimmoores.quandl.processing.tablesaw.JSONTableSawRESTDataProvider;
import com.jimmoores.quandl.processing.tablesaw.TableSawRESTDataProvider;
import com.jimmoores.quandl.util.ArgumentChecker;
import tech.tablesaw.api.Table;
package com.jimmoores.quandl.tablesaw;




public class TableSawQuandlSession extends GenericQuandlSession<MetaDataResult, JSONObject, Table, SearchResult>{
  private static final Logger LOGGER = LoggerFactory.getLogger(TableSawQuandlSession.class);
  public TableSawQuandlSession(SessionOptions sessionOptions, GenericRESTDataProvider<JSONObject, Table> restDataProvider,
      MetaDataPackager<MetaDataResult, JSONObject, SearchResult> metaDataPackager) {
    super(sessionOptions, restDataProvider, metaDataPackager);
  }
  
  /**
   * Create a Quandl session without an authorization token (authToken). An attempt will be made to read the java property
   * <em>quandl.auth.token</em> and use that if available. Any resulting SecurityException is logged at debug level, otherwise it is ignored
   * and no auth token is used. Using a token means you can make more requests. Note creating this object does not make any actual API
   * requests, the token is used in subsequent requests.
   * 
   * @return an instance of the Quandl session, not null.
   */
  public static TableSawQuandlSession create() {
    try {
      String authToken = System.getProperty(QUANDL_AUTH_TOKEN_PROPERTY_NAME);
      if (authToken != null) {
        return new TableSawQuandlSession(
            SessionOptions.Builder.withAuthToken(authToken).build(), 
            new JSONTableSawRESTDataProvider(), 
            new ClassicMetaDataPackager());