public final class DataSetRequest implements Request {
  private static final String START_DATE_PARAM = "start_date";
  private static final String END_DATE_PARAM = "end_date";
  private static final String COLUMN_INDEX_PARAM = "column_index";
  private static final String FREQUENCY_PARAM = "collapse";
  private static final String MAX_ROWS_PARAM = "limit";
  private static final String TRANSFORM_PARAM = "transform";
  private static final String SORT_ORDER_PARAM = "order";

  private static final String EXTENSION = ".csv";
  private static final String DATASETS_RELATIVE_URL = "datasets";

  private final String _quandlCode;
  private LocalDate _startDate;
  private LocalDate _endDate;
  private Integer _columnIndex;
  private Frequency _frequency;
  private Integer _maxRows;
  private Transform _transform;
  private SortOrder _sortOrder;

  private DataSetRequest(final Builder builder) {
    // Note the builder may contain nulls for some fields and that's fine, means 'not specified'.
    _quandlCode = builder._quandlCode;
    _startDate = builder._startDate;
    _endDate = builder._endDate;
    _columnIndex = builder._columnIndex;
    _frequency = builder._frequency;
    _maxRows = builder._maxRows;
    _transform = builder._transform;
    _sortOrder = builder._sortOrder;
  }

  /**
   * @return the quandl code corresponding with the request
   */
  public String getQuandlCode() {
    return _quandlCode;
  }

  /**
   * @return the Frequency that was set, or null if none specified.
   */
  public Frequency getFrequency() {
    return _frequency;
  }
  
  /**
   * @return the Start Date that was set, or null if none specified.
   */
  public LocalDate getStartDate() {
    return _startDate;
  }

  /**
   * @return the End Date that was set, or null if none specified.
   */
  public LocalDate getEndDate() {
    return _endDate;
  }
  
  /**
   * @return the column index that was set, or null if none specified.
   */
  public Integer getColumnIndex() {
    return _columnIndex;
  }
  
  /**
   * @return the maximum number of rows to return, or null if none specified.
   */
  public Integer getMaxRows() {
    return _maxRows;
  }

  /**
   * @return the transform method, or null if none specified.
   */
  public Transform getTransform() {
    return _transform;
  }

  /**
   * @return the sort order, or null if none specified.
   */
  public SortOrder getSortOrder() {
    return _sortOrder;
  }

  /**
   * Inner builder class. Create an instance using of("QUANDL/CODE"), call any other methods you need, and finish by calling build().
   */
  public static final class Builder {
    private final String _quandlCode;
    private LocalDate _startDate;
    private LocalDate _endDate;
    private Integer _columnIndex;
    private Frequency _frequency;
    private Integer _maxRows;
    private Transform _transform;
    private SortOrder _sortOrder;

    private Builder(final String quandlCode) {
      _quandlCode = quandlCode;
    }

    /**
     * Create the base DataSetRequest object passing in the Quandl code.
     * 
     * @param quandlCode the quandl code for the data you're interested in, not null
     * @return a Builder instance, not null
     */
    public static Builder of(final String quandlCode) {
      ArgumentChecker.notNull(quandlCode, "quandlCode");
      return new Builder(quandlCode);
    }

    /**
     * Optionally specify a start date cut-off for the request.
     * 
     * @param startDate the start date of the request (inclusive), not null
     * @return a Builder instance, not null
     */
    public Builder withStartDate(final LocalDate startDate) {
      ArgumentChecker.notNull(startDate, "startDate");
      _startDate = startDate;
      return this;
    }

    /**
     * Optionally specify an end date cut-off for the request.
     * 
     * @param endDate the end date of the request (inclusive), not null
     * @return a Builder instance, not null
     */
    public Builder withEndDate(final LocalDate endDate) {
      ArgumentChecker.notNull(endDate, "endDate");
      _endDate = endDate;
      return this;
    }

    /**
     * Optionally specify a specific column for the request. This can only be used once per request.
     * 
     * @param columnIndex the zero-based column index being requested
     * @return a Builder instance, not null
     */
    public Builder withColumn(final int columnIndex) {
      _columnIndex = columnIndex;
      return this;
    }

    /**
     * Optionally specify the sampling frequency for the request.
     * 
     * @param frequency the sampling frequency, not null
     * @return a Builder instance, not null
     */
    public Builder withFrequency(final Frequency frequency) {
      ArgumentChecker.notNull(frequency, "frequency");
      _frequency = frequency;
      return this;
    }

    /**
     * Optionally specify the maximum number of rows that should be returns for the request.
     * 
     * @param maxRows the maximum number of rows that the server should return from the request
     * @return a Builder instance, not null
     */
    public Builder withMaxRows(final int maxRows) {
      _maxRows = maxRows;
      return this;
    }

    /**
     * Optionally specify a data transformation function for the request.
     * 
     * @param transform the data transformation method that the server should pre-process the data with
     * @return a Builder instance, not null
     */
    public Builder withTransform(final Transform transform) {
      ArgumentChecker.notNull(transform, "transform");
      _transform = transform;
      return this;
    }

    /**
     * Optionally specify the sort order of the results.
     * 
     * @param sortOrder the sort order of the results
     * @return a Builder object onto which you can chain additional calls
     */
    public Builder withSortOrder(final SortOrder sortOrder) {
      ArgumentChecker.notNull(sortOrder, "sortOrder");
      _sortOrder = sortOrder;
      return this;
    }

    /**
     * Build the request object.
     * 
     * @return the immutable build object
     */
    public DataSetRequest build() {
      return new DataSetRequest(this);
    }
  }

  /**
   * Append any specified parameters to the provided WebTarget.
   * 
   * @param webTarget a web target used by the Jersey Client API
   * @return the WebTarget with any path and query parameters appended
   */
  public WebTarget appendPathAndQueryParameters(final WebTarget webTarget) {
    ArgumentChecker.notNull(webTarget, "webTarget");
    WebTarget resultTarget = webTarget;
    resultTarget = resultTarget.path(DATASETS_RELATIVE_URL);
    resultTarget = resultTarget.path(_quandlCode + EXTENSION);
    if (_startDate != null) {
      resultTarget = resultTarget.queryParam(START_DATE_PARAM, _startDate.toString());
    }
    if (_endDate != null) {
      resultTarget = resultTarget.queryParam(END_DATE_PARAM, _endDate.toString());
    }
    if (_columnIndex != null) {
      resultTarget = resultTarget.queryParam(COLUMN_INDEX_PARAM, _columnIndex);
    }
    if (_frequency != null) {
      resultTarget = resultTarget.queryParam(FREQUENCY_PARAM, _frequency.getQuandlString());
    }
    if (_maxRows != null) {
      resultTarget = resultTarget.queryParam(MAX_ROWS_PARAM, _maxRows);
    }
    if (_transform != null) {
      resultTarget = resultTarget.queryParam(TRANSFORM_PARAM, _transform.getQuandlString());
    }
    if (_sortOrder != null) {
      resultTarget = resultTarget.queryParam(SORT_ORDER_PARAM, _sortOrder.getQuandlString());
    }
    return resultTarget;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    if (_frequency != null) {
      result = (prime * result) + _frequency.hashCode();
    }
    if (_columnIndex != null) {
      result = (prime * result) + _columnIndex.hashCode();
    }
    if (_maxRows != null) {
      result = (prime * result) + _maxRows.hashCode();
    }
    if (_transform != null) {
      result = (prime * result) + _transform.hashCode();
    }
    if (_sortOrder != null) {
      result = (prime * result) + _sortOrder.hashCode();
    }
    if (_endDate != null) {
      result = (prime * result) + _endDate.hashCode();
    }
    if (_startDate != null) {
      result = (prime * result) + _startDate.hashCode();
    }
    // make sure most important fields in low order bits in case of integer overflow
    if (_quandlCode != null) {
      result = (prime * result) + _quandlCode.hashCode();
    }
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof DataSetRequest)) {
      return false;
    }
    DataSetRequest other = (DataSetRequest) obj;
    if (_quandlCode == null) {
      if (other._quandlCode != null) {
        return false;
      }
    } else if (!_quandlCode.equals(other._quandlCode)) {
      return false;
    }
    if (_columnIndex == null) {
      if (other._columnIndex != null) {
        return false;
      }
    } else if (!_columnIndex.equals(other._columnIndex)) {
      return false;
    }
    if (_endDate == null) {
      if (other._endDate != null) {
        return false;
      }
    } else if (!_endDate.equals(other._endDate)) {
      return false;
    }
    if (_frequency != other._frequency) {
      return false;
    }
    if (_maxRows == null) {
      if (other._maxRows != null) {
        return false;
      }
    } else if (!_maxRows.equals(other._maxRows)) {
      return false;
    }

    if (_sortOrder != other._sortOrder) {
      return false;
    }
    if (_startDate == null) {
      if (other._startDate != null) {
        return false;
      }
    } else if (!_startDate.equals(other._startDate)) {
      return false;
    }
    if (_transform != other._transform) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("DataSetRequest[quandlCode=");
    builder.append(_quandlCode);
    builder.append(", startDate=");
    builder.append(_startDate);
    builder.append(", endDate=");
    builder.append(_endDate);
    builder.append(", columnIndex=");
    builder.append(_columnIndex);
    builder.append(", frequency=");
    builder.append(_frequency);
    builder.append(", maxRows=");
    builder.append(_maxRows);
    builder.append(", transform=");
    builder.append(_transform);
    builder.append(", sortOrder=");
    builder.append(_sortOrder);
    builder.append("]");
    return builder.toString();
  }
  
  /**
   * Accept a request processor in visitor pattern style.
   * @param <T> the processor result type
   * @param processor  the request processor
   * @return the request processor's result
   */
  public <T> T accept(final RequestProcessor<T> processor) {
    return processor.processDataSetRequest(this);
  }
} public enum Frequency {
  /**
   * Use underlying data frequency.
   */
  NONE("none"),
  /**
   * Daily sampling.
   */
  DAILY("daily"),
  /**
   * Weekly sampling.
   */
  WEEKLY("weekly"),
  /**
   * Monthly sampling.
   */
  MONTHLY("monthly"),
  /**
   * Quarterly sampling.
   */
  QUARTERLY("quarterly"),
  /**
   * Annual sampling.
   */
  ANNUAL("annual");

  private String _quandlString;

  /**
   * Constructor to augment enum with correct quandl API string.
   * 
   * @param quandlString string to pass to Quandl REST API
   */
  Frequency(final String quandlString) {
    ArgumentChecker.notNullOrEmpty(quandlString, "quandlString");
    _quandlString = quandlString;
  }

  /**
   * Get the string to pass to the Quandl REST API associated with this enum.
   * 
   * @return the correct string to pass to the Quandl REST API, not null.
   */
  String getQuandlString() {
    return _quandlString;
  }
} public final class HeaderDefinition {
  private static Logger s_logger = LoggerFactory.getLogger(HeaderDefinition.class);
  private final Map<String, Integer> _columnNamesToIndices = new LinkedHashMap<String, Integer>();
  private final List<String> _columnNames;

  private HeaderDefinition(final List<String> columnNames) {
    _columnNames = new ArrayList<String>(columnNames);
    int i = 0;
    Map<String, Integer> duplicateCounters = new HashMap<String, Integer>();
    for (String columnName : columnNames) {
      if (_columnNamesToIndices.containsKey(columnName)) {
        int count;
        if (duplicateCounters.containsKey(columnName)) {
          count = duplicateCounters.get(columnName);
          duplicateCounters.put(columnName, count + 1);
        } else {
          duplicateCounters.put(columnName, 1);
          count = 1;
        }
        String arrayColumnName = columnName + "." + count;
        s_logger.warn("Duplicate column name {} enountered, renaming {}", columnName, arrayColumnName);
        _columnNamesToIndices.put(arrayColumnName, i);
        _columnNames.set(i, arrayColumnName);
        i++;
      } else {
        _columnNamesToIndices.put(columnName, i++);
      }
    }

  }

  /**
   * Create a RowDefinition. In rare situations, duplicates can occur. These will be renamed so:
   * 
   * <pre>
   *   column, column, column, ...
   * </pre>
   * 
   * becomes:
   * 
   * <pre>
   *   column, column.1, column.2, ...
   * </pre>
   * 
   * for lookup and display purposes. A warning will be printed to the logger.
   * 
   * @param columnNames a list of strings, each naming a column
   * @return the instance
   */
  public static HeaderDefinition of(final List<String> columnNames) {
    ArgumentChecker.notNull(columnNames, "columnNames");
    return new HeaderDefinition(columnNames);
  }

  /**
   * Create a RowDefinition when no type information is available. In rare situations, duplicates can occur. These will be renamed so:
   * 
   * <pre>
   *   column, column, column, ...
   * </pre>
   * 
   * becomes:
   * 
   * <pre>
   *   column, column.1, column.2, ...
   * </pre>
   * 
   * for lookup purposes. A warning will be printed to the logger.
   * 
   * @param columnNames a vararg array of strings, each naming a column
   * @return the instance
   */
  public static HeaderDefinition of(final String... columnNames) {
    ArgumentChecker.notNull(columnNames, "columnNames");
    return new HeaderDefinition(Arrays.asList(columnNames));
  }

  /**
   * Get the column index of the named column (zero-based). Throws IllegalArgumentException if column of provided name is not found.
   * Repeated column names can be indexed as column, column.1, column.2, etc.
   * 
   * @param columnName the name of the column
   * @return the column index
   * 
   */
  public int columnIndex(final String columnName) {
    Integer index = _columnNamesToIndices.get(columnName);
    if (index == null) {
      throw new IllegalArgumentException("Could not find index for column " + columnName);
    }
    return index;
  }

  /**
   * Get the number of columns in this header definition.
   * 
   * @return the number of columns
   */
  public int size() {
    return _columnNames.size();
  }

  /**
   * Get an iterator to return the names of all the columns in order.
   * 
   * @return the iterator
   */
  public Iterator<String> iterator() {
    return getColumnNames().iterator();
  }

  /**
   * Get an immutable copy of the list of column names.
   * 
   * @return an immutable copy of the list of column names
   */
  public List<String> getColumnNames() {
    return Collections.unmodifiableList(_columnNames);
  }

  @Override
  public int hashCode() {
    return _columnNames.hashCode();
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof HeaderDefinition)) {
      return false;
    }
    HeaderDefinition other = (HeaderDefinition) obj;
    if (_columnNames == null) {
      if (other._columnNames != null) {
        return false;
      }
    } else if (!_columnNames.equals(other._columnNames)) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("HeaderDefinition[");
    Iterator<String> iter = _columnNames.iterator();
    while (iter.hasNext()) {
      sb.append(iter.next());
      if (iter.hasNext()) {
        sb.append(",");
      }
    }
    sb.append("]");
    return sb.toString();
  }
} public final class MetaDataRequest implements Request {
  private static final String EXTENSION = ".json";
  private static final String DATASETS_RELATIVE_URL = "datasets";
  private static final String EXCLUDE_DATA_PARAM = "start_date";
  private static final String INFINITE_FUTURE = "2100-01-01";

  private String _quandlCode;

  private MetaDataRequest(final String quandlCode) {
    _quandlCode = quandlCode;
  }

  /**
   * Factory method to create a meta data request instance.
   * 
   * @param quandlCode the quandl code for the meta data required, not null
   * @return an instance of the MetaDataRequest for the given quandlCode
   */
  public static MetaDataRequest of(final String quandlCode) {
    ArgumentChecker.notNull(quandlCode, "quandlCode");
    return new MetaDataRequest(quandlCode);
  }

  /**
   * @return the quandl code
   */
  public String getQuandlCode() {
    return _quandlCode;
  }

  /**
   * Append any specified parameters to the provided WebTarget.
   * 
   * @param webTarget a web target used by the Jersey Client API, not null
   * @return the WebTarget with any path and query parameters appended
   */
  public WebTarget appendPathAndQueryParameters(final WebTarget webTarget) {
    ArgumentChecker.notNull(webTarget, "webTarget");
    WebTarget resultTarget = webTarget;
    resultTarget = resultTarget.path(DATASETS_RELATIVE_URL);
    resultTarget = resultTarget.path(_quandlCode + EXTENSION);
    resultTarget = resultTarget.queryParam(EXCLUDE_DATA_PARAM, INFINITE_FUTURE);
    return resultTarget;
  }

  @Override
  public int hashCode() {
    return _quandlCode.hashCode();
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof MetaDataRequest)) {
      return false;
    }
    MetaDataRequest other = (MetaDataRequest) obj;
    return _quandlCode.equals(other._quandlCode);
  }

  @Override
  public String toString() {
    return "MetaDataRequest[quandlCode=" + _quandlCode + "]";
  }
  
  /**
   * Accept a request processor in visitor pattern style.
   * @param <T> the processor result type
   * @param processor  the request processor
   * @return the request processor's result
   */
  public <T> T accept(final RequestProcessor<T> processor) {
    return processor.processMetaDataRequest(this);
  }
} 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 MultiDataSetRequest implements Request {
  private static final String COLUMNS_PARAM = "columns";
  private static final String START_DATE_PARAM = "start_date";
  private static final String END_DATE_PARAM = "end_date";
  private static final String FREQUENCY_PARAM = "collapse";
  private static final String MAX_ROWS_PARAM = "limit";
  private static final String TRANSFORM_PARAM = "transform";
  private static final String SORT_ORDER_PARAM = "order";

  private static final String MULTI_SET_NAME = "multisets";
  private static final String EXTENSION = ".csv";

  private final List<QuandlCodeRequest> _quandlCodeRequests;
  private final LocalDate _startDate;
  private final LocalDate _endDate;
  private final Frequency _frequency;
  private final Integer _maxRows;
  private final Transform _transform;
  private final SortOrder _sortOrder;

  private MultiDataSetRequest(final Builder builder) {
    _quandlCodeRequests = builder._quandlCodeRequests;
    _startDate = builder._startDate;
    _endDate = builder._endDate;
    _frequency = builder._frequency;
    _maxRows = builder._maxRows;
    _transform = builder._transform;
    _sortOrder = builder._sortOrder;
  }

  /**
   * Inner builder class. Create an instance using of("QUANDL/CODE"), call any other methods you need, and finish by calling build(). Note
   * you are be able to use a slash as opposed to the period separator as specified in the Quandl REST API docs, but either will work here.
   */
  public static final class Builder {
    private final List<QuandlCodeRequest> _quandlCodeRequests;
    private LocalDate _startDate;
    private LocalDate _endDate;
    private Frequency _frequency;
    private Integer _maxRows;
    private Transform _transform;
    private SortOrder _sortOrder;

    private Builder(final List<QuandlCodeRequest> quandlCodeRequests) {
      _quandlCodeRequests = quandlCodeRequests;
    }

    /**
     * Create the base DataSetRequest object passing in the Quandl code.
     * 
     * @param quandlCodeRequests the list of quandl codes/columns you're interested in, not null
     * @return a Builder instance, not null
     */
    public static Builder of(final List<QuandlCodeRequest> quandlCodeRequests) {
      ArgumentChecker.notNullOrEmpty(quandlCodeRequests, "quandlCodeRequests");
      return new Builder(quandlCodeRequests);
    }

    /**
     * Create the base DataSetRequest object passing in the Quandl code.
     * 
     * @param quandlCodeRequests the quandl codes/columns (VarArgs) you're interested in, not null
     * @return a Builder instance, not null
     */
    public static Builder of(final QuandlCodeRequest... quandlCodeRequests) {
      ArgumentChecker.notNullOrEmpty(quandlCodeRequests, "quandlCodeRequests");
      return new Builder(Arrays.asList(quandlCodeRequests));
    }

    /**
     * Optionally specify a start date cut-off for the request.
     * 
     * @param startDate the start date of the request (inclusive), not null
     * @return a Builder instance, not null
     */
    public Builder withStartDate(final LocalDate startDate) {
      ArgumentChecker.notNull(startDate, "startDate");
      _startDate = startDate;
      return this;
    }

    /**
     * Optionally specify an end date cut-off for the request.
     * 
     * @param endDate the end date of the request (inclusive), not null
     * @return a Builder instance, not null
     */
    public Builder withEndDate(final LocalDate endDate) {
      ArgumentChecker.notNull(endDate, "endDate");
      _endDate = endDate;
      return this;
    }

    /**
     * Optionally specify the sampling frequency for the request.
     * 
     * @param frequency the sampling frequency, not null
     * @return a Builder instance, not null
     */
    public Builder withFrequency(final Frequency frequency) {
      ArgumentChecker.notNull(frequency, "frequency");
      _frequency = frequency;
      return this;
    }

    /**
     * Optionally specify the maximum number of rows that should be returns for the request.
     * 
     * @param maxRows the maximum number of rows that the server should return from the request
     * @return a Builder instance, not null
     */
    public Builder withMaxRows(final int maxRows) {
      _maxRows = maxRows;
      return this;
    }

    /**
     * Optionally specify a data transformation function for the request.
     * 
     * @param transform the data transformation method that the server should pre-process the data with
     * @return a Builder instance, not null
     */
    public Builder withTransform(final Transform transform) {
      ArgumentChecker.notNull(transform, "transform");
      _transform = transform;
      return this;
    }

    /**
     * Optionally specify the sort order of the results.
     * 
     * @param sortOrder the sort order of the results
     * @return a Builder object onto which you can chain additional calls
     */
    public Builder withSortOrder(final SortOrder sortOrder) {
      ArgumentChecker.notNull(sortOrder, "sortOrder");
      _sortOrder = sortOrder;
      return this;
    }

    /**
     * Build the request object.
     * 
     * @return the immutable build object
     */
    public MultiDataSetRequest build() {
      return new MultiDataSetRequest(this);
    }
  }

  /**
   * @return the list of quandl code requests (code + column)
   */
  public List<QuandlCodeRequest> getQuandlCodeRequests() {
    return _quandlCodeRequests;
  }

  /**
   * @return the start date, or null if not set
   */
  public LocalDate getStartDate() {
    return _startDate;
  }

  /**
   * @return the end date, or null if not set
   */
  public LocalDate getEndDate() {
    return _endDate;
  }

  /**
   * @return the frequency, or null if not set
   */
  public Frequency getFrequency() {
    return _frequency;
  }

  /**
   * @return the maximum number of rows to be returned, or null if not set
   */
  public Integer getMaxRows() {
    return _maxRows;
  }

  /**
   * @return the transform requested, or null if not set
   */
  public Transform getTransform() {
    return _transform;
  }

  /**
   * @return the sort order, or null if not set
   */
  public SortOrder getSortOrder() {
    return _sortOrder;
  }

  private String buildCodeList(final List<QuandlCodeRequest> quandlCodeRequests) {
    StringBuilder sb = new StringBuilder();
    Iterator<QuandlCodeRequest> iter = quandlCodeRequests.iterator();
    while (iter.hasNext()) {
      QuandlCodeRequest req = iter.next();
      sb.append(req.getQuandlCode().replace('/', '.')); // allow user to use WIKI/AAPL as all other calls rather than WIKI.AAPL.
      if (req.isSingleColumnRequest()) { // append the column number.
        sb.append(".");
        sb.append(req.getColumnNumber());
      }
      if (iter.hasNext()) {
        sb.append(",");
      }
    }
    return sb.toString();
  }

  /**
   * Append any specified parameters to the provided WebTarget.
   * 
   * @param webTarget a web target used by the Jersey Client API, not null
   * @return the WebTarget with any path and query parameters appended, not null
   */
  public WebTarget appendPathAndQueryParameters(final WebTarget webTarget) {
    ArgumentChecker.notNull(webTarget, "webTarget");
    WebTarget resultTarget = webTarget;
    resultTarget = resultTarget.path(MULTI_SET_NAME + EXTENSION);
    resultTarget = resultTarget.queryParam(COLUMNS_PARAM, buildCodeList(_quandlCodeRequests));
    if (_startDate != null) {
      resultTarget = resultTarget.queryParam(START_DATE_PARAM, _startDate.toString());
    }
    if (_endDate != null) {
      resultTarget = resultTarget.queryParam(END_DATE_PARAM, _endDate.toString());
    }
    if (_frequency != null) {
      resultTarget = resultTarget.queryParam(FREQUENCY_PARAM, _frequency.getQuandlString());
    }
    if (_maxRows != null) {
      resultTarget = resultTarget.queryParam(MAX_ROWS_PARAM, _maxRows);
    }
    if (_transform != null) {
      resultTarget = resultTarget.queryParam(TRANSFORM_PARAM, _transform.getQuandlString());
    }
    if (_sortOrder != null) {
      resultTarget = resultTarget.queryParam(SORT_ORDER_PARAM, _sortOrder.getQuandlString());
    }
    return resultTarget;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    if (_frequency != null) {
      result = (prime * result) + _frequency.hashCode();
    }
    if (_maxRows != null) {
      result = (prime * result) + _maxRows.hashCode();
    }
    if (_transform != null) {
      result = (prime * result) + _transform.hashCode();
    }
    if (_sortOrder != null) {
      result = (prime * result) + _sortOrder.hashCode();
    }
    if (_startDate != null) {
      result = (prime * result) + _startDate.hashCode();
    }
    if (_endDate != null) {
      result = (prime * result) + _endDate.hashCode();
    }
    // this can't be null, so don't need to test.
    result = (prime * result) + _quandlCodeRequests.hashCode();
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof MultiDataSetRequest)) {
      return false;
    }
    MultiDataSetRequest other = (MultiDataSetRequest) obj;
    if (!_quandlCodeRequests.equals(other._quandlCodeRequests)) {
      return false;
    }
    if (_startDate == null) {
      if (other._startDate != null) {
        return false;
      }
    } else if (!_startDate.equals(other._startDate)) {
      return false;
    }
    if (_endDate == null) {
      if (other._endDate != null) {
        return false;
      }
    } else if (!_endDate.equals(other._endDate)) {
      return false;
    }
    if (_frequency != other._frequency) {
      return false;
    }
    if (_maxRows == null) {
      if (other._maxRows != null) {
        return false;
      }
    } else if (!_maxRows.equals(other._maxRows)) {
      return false;
    }

    if (_sortOrder != other._sortOrder) {
      return false;
    }

    if (_transform != other._transform) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("MultiDataSetRequest[quandlCodeRequests=");
    builder.append(_quandlCodeRequests);
    builder.append(", startDate=");
    builder.append(_startDate);
    builder.append(", endDate=");
    builder.append(_endDate);
    builder.append(", maxRows=");
    builder.append(_maxRows);
    builder.append(", frequency=");
    builder.append(_frequency);
    builder.append(", transform=");
    builder.append(_transform);
    builder.append(", sortOrder=");
    builder.append(_sortOrder);
    builder.append("]");
    return builder.toString();
  }

  /**
   * Accept a request processor in visitor pattern style.
   * @param <T> the processor result type
   * @param processor  the request processor
   * @return the request processor's result
   */
  public <T> T accept(final RequestProcessor<T> processor) {
    return processor.processMultiDataSetRequest(this);
  }
} public final class MultiMetaDataRequest implements Request {
  private static final String COLUMNS_PARAM = "columns";
  private static final String EXTENSION = ".json";
  private static final String MULTI_SET_NAME = "multisets";
  private static final String EXCLUDE_DATA_PARAM = "start_date";
  private static final String INFINITE_FUTURE = "2100-01-01";

  private List<String> _quandlCodes;

  private MultiMetaDataRequest(final List<String> quandlCodes) {
    _quandlCodes = Collections.unmodifiableList(new ArrayList<String>(quandlCodes));
  }

  /**
   * Factory method to create a meta data request instance.
   * 
   * @param quandlCodes the list of quandl codes for the meta data required, not null
   * @return an instance of the MetaDataRequest for the given quandlCode, not null
   */
  public static MultiMetaDataRequest of(final List<String> quandlCodes) {
    ArgumentChecker.notNullOrEmpty(quandlCodes, "quandlCodes");
    return new MultiMetaDataRequest(quandlCodes);
  }

  /**
   * Factory method to create a meta data request instance.
   * 
   * @param quandlCodes the list of quandl codes for the meta data required (varargs), not null
   * @return an instance of the MetaDataRequest for the given quandlCode, not null
   */
  public static MultiMetaDataRequest of(final String... quandlCodes) {
    ArgumentChecker.notNullOrEmpty(quandlCodes, "quandlCodes");
    return new MultiMetaDataRequest(Arrays.asList(quandlCodes));
  }

  /**
   * @return the list of quandl codes contained in this request.
   */
  public List<String> getQuandlCodes() {
    return _quandlCodes;
  }

  private String buildCodeList(final List<String> quandlCodes) {
    StringBuilder sb = new StringBuilder();
    Iterator<String> iter = quandlCodes.iterator();
    while (iter.hasNext()) {
      String quandlCode = iter.next();
      if (quandlCode == null) {
        throw new IllegalArgumentException("There was a null encountered in the argument list " + quandlCodes);
      }
      String transformedCode = quandlCode.replace('/', '.');
      sb.append(transformedCode);
      if (iter.hasNext()) {
        sb.append(",");
      }
    }
    return sb.toString();
  }

  /**
   * Append any specified parameters to the provided WebTarget.
   * 
   * @param webTarget a web target used by the Jersey Client API, not null
   * @return the WebTarget with any path and query parameters appended, not null
   */
  public WebTarget appendPathAndQueryParameters(final WebTarget webTarget) {
    ArgumentChecker.notNull(webTarget, "webTarget");
    WebTarget resultTarget = webTarget;
    resultTarget = resultTarget.path(MULTI_SET_NAME + EXTENSION);
    resultTarget = resultTarget.queryParam(COLUMNS_PARAM, buildCodeList(_quandlCodes));
    // This is a hack that stops Quandl from returning all the data as part of the query
    resultTarget = resultTarget.queryParam(EXCLUDE_DATA_PARAM, INFINITE_FUTURE);
    return resultTarget;
  }

  @Override
  public int hashCode() {
    return _quandlCodes.hashCode();
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof MultiMetaDataRequest)) {
      return false;
    }
    MultiMetaDataRequest other = (MultiMetaDataRequest) obj;
    if (!_quandlCodes.equals(other._quandlCodes)) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    return "MultiMetaDataRequest[" + _quandlCodes + "]";
  }
  
  /**
   * Accept a request processor in visitor pattern style.
   * @param <T> the processor result type
   * @param processor  the request processor
   * @return the request processor's result
   */
  public <T> T accept(final RequestProcessor<T> processor) {
    return processor.processMultiMetaDataRequest(this);
  }
} public final class QuandlSession implements LegacyQuandlSession<MetaDataResult, TabularResult, SearchResult> {
  private static final String JSON_TO_DATE_FIELD = "to_date";
  private static final String JSON_FROM_DATE_FIELD = "from_date";
  private static final String JSON_FREQUENCY_FIELD = "frequency";
  private static final String JSON_ERRORS_FIELD = "errors";
  private static final String JSON_DATA_FIELD = "data";
  private static final String JSON_COLUMNS_FIELD = "columns";
  private static final String JSON_COLUMN_NAMES_FIELD = "column_names";
  private static final String DATE_COLUMN = "Date";
  private static final UriBuilder API_BASE_URL_V3 = UriBuilder.fromPath("https://www.quandl.com/api/v3");
  private static Logger s_logger = LoggerFactory.getLogger(QuandlSession.class);

  private SessionOptions _sessionOptions;
  private GenericRESTDataProvider<JSONObject, TabularResult> _restDataProvider;

  private QuandlSession(final SessionOptions sessionOptions) {
    _sessionOptions = sessionOptions;
    _restDataProvider = LegacyRESTDataProviderAdapter.of(_sessionOptions.getRESTDataProvider());
  }

  /**
   * Create a Quandl session to use a specific authorization token (authToken) with all requests. Using a token means you can make more
   * requests. Note that this method does not check the quandl.auth.token property. Creating this object does not make any actual API
   * requests, the token is used in subsequent requests.
   * 
   * @param authToken a Quandl API authorization token
   * @return an instance of the Quandl session with an embedded authorization token
   */
  public static QuandlSession create(final String authToken) {
    ArgumentChecker.notNull(authToken, "authToken");
    return new QuandlSession(SessionOptions.Builder.withAuthToken(authToken).build());
  }

  /**
   * 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 QuandlSession create() {
    try {
      String authToken = System.getProperty(GenericQuandlSession.QUANDL_AUTH_TOKEN_PROPERTY_NAME);
      if (authToken != null) {
        return new QuandlSession(SessionOptions.Builder.withAuthToken(authToken).build());
      }
    } catch (SecurityException se) {
      s_logger.debug("Error accessing system property " + GenericQuandlSession.QUANDL_AUTH_TOKEN_PROPERTY_NAME + ", falling back to not using an auth token",
          se);
    }
    return new QuandlSession(SessionOptions.Builder.withoutAuthToken().build());
  }

  /**
   * Create a Quandl session with detailed SessionOptions. No attempt will be made to read the java property <em>quandl.auth.token</em> even
   * if available. Note creating this object does not make any actual API requests, the token is used in subsequent requests.
   * 
   * @param sessionOptions a user created SessionOptions instance, not null
   * @return an instance of the Quandl session, not null
   */
  public static QuandlSession create(final SessionOptions sessionOptions) {
    ArgumentChecker.notNull(sessionOptions, "sessionOptions");
    return new QuandlSession(sessionOptions);
  }

  /**
   * 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;
    }
  }

  @Override
  public TabularResult getDataSet(final DataSetRequest request) {
    ArgumentChecker.notNull(request, "request");

    Client client = getClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = withAuthToken(target);
    target = request.appendPathAndQueryParameters(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 MetaDataResult getMetaData(final MetaDataRequest request) {
    ArgumentChecker.notNull(request, "request");
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = withAuthToken(target);
    target = request.appendPathAndQueryParameters(target);
    JSONObject object = null;
    int retries = 0;
    QuandlRuntimeException lastException;
    do {
      try {
        return MetaDataResult.of(_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 (object == null && _sessionOptions.getRetryPolicy().checkRetries(retries++));

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

  @Override
  public TabularResult getDataSets(final MultiDataSetRequest request) {
    final List<QuandlCodeRequest> quandlCodeRequests = request.getQuandlCodeRequests();
    final Map<QuandlCodeRequest, TabularResult> results = new LinkedHashMap<QuandlCodeRequest, TabularResult>();
    for (final QuandlCodeRequest quandlCodeRequest : quandlCodeRequests) {
      final Builder builder = DataSetRequest.Builder.of(quandlCodeRequest.getQuandlCode());
      if (quandlCodeRequest.isSingleColumnRequest()) {
        builder.withColumn(quandlCodeRequest.getColumnNumber());
      }
      if (request.getEndDate() != null) {
        builder.withEndDate(request.getEndDate());
      }
      if (request.getStartDate() != null) {
        builder.withStartDate(request.getStartDate());
      }
      if (request.getFrequency() != null) {
        builder.withFrequency(request.getFrequency());
      }
      if (request.getMaxRows() != null) {
        builder.withMaxRows(request.getMaxRows());
      }
      if (request.getSortOrder() != null) {
        builder.withSortOrder(request.getSortOrder());
      }
      if (request.getTransform() != null) {
        builder.withTransform(request.getTransform());
      }
      final DataSetRequest dataSetRequest = builder.build();
      TabularResult tabularResult = null;
      try {
        tabularResult = getDataSet(dataSetRequest);
      } catch (final QuandlRuntimeException qre) {
        s_logger.error("Exception processing request for {}, giving up and skipping.  Full request was {}",
            quandlCodeRequest.getQuandlCode(), dataSetRequest, qre);
        continue;
      }
      if (tabularResult != null) {
        results.put(quandlCodeRequest, tabularResult);
      } else {
        s_logger.error("Can't process request for {}, returned null.  Giving up and skipping.  Full request was {}",
            quandlCodeRequest.getQuandlCode(), dataSetRequest);
      }
    }
    return mergeTables(results, request.getSortOrder());
  }

  private TabularResult mergeTables(final Map<QuandlCodeRequest, TabularResult> results, final SortOrder sortOrder) {
    int resultTableWidth = 1; // the date!
    final Map<QuandlCodeRequest, Integer> initialOffset = new HashMap<QuandlCodeRequest, Integer>();
    final List<String> columnNames = new ArrayList<String>();
    columnNames.add("Date");
    for (final Map.Entry<QuandlCodeRequest, TabularResult> entry : results.entrySet()) {
      final QuandlCodeRequest codeRequest = entry.getKey();
      final TabularResult table = entry.getValue();
      if (!initialOffset.containsKey(codeRequest)) {
        initialOffset.put(codeRequest, resultTableWidth); // record the offset for each table
      }
      resultTableWidth += table.getHeaderDefinition().size() - 1; // exclude the date column.
      final List<String> names = table.getHeaderDefinition().getColumnNames();
      final Iterator<String> iter = names.iterator();
      if (!iter.hasNext()) {
        throw new QuandlRuntimeException("table has no columns, expected at least date");
      }
      iter.next(); // discard date column name
      while (iter.hasNext()) {
        final String colName = iter.next();
        columnNames.add(codeRequest.getQuandlCode() + " - " + colName);
      }
    }
    final Comparator<ChronoLocalDate> comparator;
    if (sortOrder == SortOrder.ASCENDING) {
      comparator = ChronoLocalDate.timeLineOrder();
    } else {
      comparator = Collections.reverseOrder(ChronoLocalDate.timeLineOrder());
    }
    final SortedMap<LocalDate, String[]> rows = new TreeMap<LocalDate, String[]>(comparator);
    for (final Map.Entry<QuandlCodeRequest, TabularResult> mapEntry : results.entrySet()) {
      final QuandlCodeRequest codeRequest = mapEntry.getKey();
      final TabularResult table1 = mapEntry.getValue();
      final Iterator<Row> rowIter = table1.iterator();
      while (rowIter.hasNext()) {
        final Row row = rowIter.next();
        final LocalDate date = row.getLocalDate(0);
        final String dateStr = row.getString(0);
        if (date != null) {
          String[] bigRow;
          if (rows.containsKey(date)) {
            bigRow = rows.get(date);
          } else {
            bigRow = new String[resultTableWidth];
            rows.put(date, bigRow);
          }
          for (int i = 1; i < row.size(); i++) {
            bigRow[initialOffset.get(codeRequest) + (i - 1)] = row.getString(i); // (i-1 is becuase initialOffset index already includes
                                                                                 // initial 1 offset)
          }
          bigRow[0] = dateStr; // (re)write the date string at the start of the big table.
        }
      }
    }
    final List<Row> combinedRows = new ArrayList<Row>();
    final HeaderDefinition headerDefinition = HeaderDefinition.of(columnNames);
    for (final Entry<LocalDate, String[]> entry : rows.entrySet()) {
      final Row row = Row.of(headerDefinition, entry.getValue());
      combinedRows.add(row);
    }
    return TabularResult.of(headerDefinition, combinedRows);
  }

  @Override
  public MetaDataResult getMetaData(final MultiMetaDataRequest request) {
    ArgumentChecker.notNull(request, "request");
    Map<String, HeaderDefinition> multipleHeaderDefinition = getMultipleHeaderDefinition(request);
    try {
      JSONObject result = new JSONObject();
      if (multipleHeaderDefinition.size() > 0) {
        result.append(JSON_COLUMN_NAMES_FIELD, DATE_COLUMN);
        result.append(JSON_COLUMNS_FIELD, DATE_COLUMN);
      }
      for (Map.Entry<String, HeaderDefinition> entry : multipleHeaderDefinition.entrySet()) {
        String quandlCode = entry.getKey();
        HeaderDefinition headerDef = entry.getValue();
        for (String columnName : headerDef.getColumnNames()) {
          if (!columnName.equals(DATE_COLUMN)) { // skip Date column for each data set.
            result.append(JSON_COLUMN_NAMES_FIELD, quandlCode + " - " + columnName);
            result.append(JSON_COLUMNS_FIELD, columnName);
          }
        }
      }
      result.put(JSON_DATA_FIELD, new JSONArray());
      result.put(JSON_ERRORS_FIELD, Collections.emptyMap());
      result.put(JSON_FREQUENCY_FIELD, (Object) null);
      result.put(JSON_FROM_DATE_FIELD, (Object) null);
      result.put(JSON_TO_DATE_FIELD, (Object) null);
      return MetaDataResult.of(result);
    } catch (JSONException ex) {
      throw new QuandlRuntimeException("Problem building JSON response", ex);
    }
  }

  @Override
  public Map<String, HeaderDefinition> getMultipleHeaderDefinition(final MultiMetaDataRequest request) {
    final Map<String, HeaderDefinition> bulkMetaData = new LinkedHashMap<String, HeaderDefinition>();
    for (final String quandlCode : request.getQuandlCodes()) {
      try {
        MetaDataResult metaData = getMetaData(MetaDataRequest.of(quandlCode));
        bulkMetaData.put(quandlCode, metaData.getHeaderDefinition());
      } catch (final QuandlRuntimeException qre) {
        s_logger.error("There was a problem requesting metadata for {}, skipping", quandlCode, qre);
        continue;
      }
    }
    return bulkMetaData;
  }

  @Override
  public SearchResult search(final SearchRequest request) {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(API_BASE_URL_V3);
    target = withAuthToken(target);
    target = request.appendPathAndQueryParameters(target);
    int retries = 0;
    QuandlRuntimeException lastException;
    do {
      try {
        return SearchResult.of(_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 final class Row {
  private static Logger s_logger = LoggerFactory.getLogger(Row.class);
  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;

  private final HeaderDefinition _headerDefinition;
  private final String[] _values;

  private Row(final HeaderDefinition headerDefinition, final String[] values) {
    if (headerDefinition.size() != values.length) {
      s_logger.error("Attempt to create a Row with a header definition containing {} columns and a values array containing {} values",
          headerDefinition.size(), values.length);
      throw new QuandlRuntimeException("headerDefinition and values array are of differing length");
    }
    _headerDefinition = headerDefinition;
    _values = values;
  }

  /**
   * Create a Row. Items in the values array can be null although the array cannot. If the headerDefinition has a different number of
   * columns than the length of the values array then an IllegalArgumentException will be thrown.
   * 
   * @param headerDefinition the row definition, not null
   * @param values the actual values in the row, not null
   * @return a row instance
   */
  public static Row of(final HeaderDefinition headerDefinition, final String[] values) {
    ArgumentChecker.notNull(headerDefinition, "headerDefinition");
    ArgumentChecker.notNull(values, "values");
    return new Row(headerDefinition, values);
  }

  /**
   * Returns an entry from a given column index as a String (possibly null). Throws an ArrayIndexOutOfBoundsException if the index is
   * greater than the number of columns in the header definition
   * 
   * @param index the index of the entry, zero-based.
   * @return the entry, can be null
   */
  public String getString(final int index) {
    return _values[index];
  }

  /**
   * Returns an entry from a given column name as a String (possibly null). Throws an IllegalArgumentException if the name is not defined in
   * the header definition. Empty is returned as the empty String rather than null.
   * 
   * @param column the column name of the entry, not null
   * @return the entry, can be null
   */
  public String getString(final String column) {
    return _values[_headerDefinition.columnIndex(column)];
  }

  /**
   * Returns an entry from a given column index as a LocalDate (possibly null). Throws an ArrayIndexOutOfBoundsException if the index is
   * greater than the number of columns in the header definition. Throws a DateTimeParseException if the underlying data is not a date in
   * ISO local date format (YYYY-MM-DD).
   * 
   * @param index the index of the entry, zero-based.
   * @return the entry, can be null if the value is null or empty
   */
  public LocalDate getLocalDate(final int index) {
    if (_values[index] == null || _values[index].isEmpty()) {
      return null;
    } else {
      return LocalDate.parse(_values[index], DATE_FORMATTER);
    }
  }

  /**
   * Returns an entry from a given column name as a LocalDate (possibly null). Throws an IllegalArgumentException if the name is not defined
   * in the header definition. Throws a DateTimeParseException if the underlying data is not a date in ISO local date format (YYYY-MM-DD).
   * 
   * @param column the column name of the entry, not null
   * @return the entry, can be null if the value is null or empty
   */
  public LocalDate getLocalDate(final String column) {
    int index = _headerDefinition.columnIndex(column);
    return getLocalDate(index);
  }

  /**
   * Returns an entry from a given column index as a Double (possibly null). Throws as ArrayIndexOutOfBoundsException if the index is
   * greater than the number of columns in the header definition. Throws a NumberFormatException if the underlying data can not be parsed as
   * a double precision floating point number.
   * 
   * @param index the index of the entry, zero-based
   * @return the value or null if the value is null or empty
   */
  public Double getDouble(final int index) {
    if (_values[index] == null || _values[index].isEmpty()) {
      return null;
    } else {
      return Double.parseDouble(_values[index]);
    }
  }

  /**
   * Returns an entry from a given column name as a Double (possibly null). Throws an IllegalArgumentException if the name is not defined in
   * the header definition. Throws a NumberFormatException if the underlying data cannot be parsed as a double precision floating point
   * number.
   * 
   * @param column the column name of the entry, not null
   * @return the entry, can be null if the value is null or empty
   */
  public Double getDouble(final String column) {
    int index = _headerDefinition.columnIndex(column);
    return getDouble(index);
  }

  /**
   * Get the number of columns in this row.
   * 
   * @return the number of columns in this row
   */
  public int size() {
    return _values.length;
  }

  /**
   * Create row with new header.
   * 
   * @param headerDefinition  the header definition
   * @return a new Row with the new header definition
   */
  public Row withPaddedHeader(final HeaderDefinition headerDefinition) {
    if (_headerDefinition != headerDefinition) {
      String[] values = new String[headerDefinition.size()];
      System.arraycopy(_values, 0, values, 0, _values.length);
      return Row.of(headerDefinition, values);
    } else {
      return this;
    }
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + _headerDefinition.hashCode();
    result = prime * result + Arrays.hashCode(_values);
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof Row)) {
      return false;
    }
    Row other = (Row) obj;
    if (!_headerDefinition.equals(other._headerDefinition)) {
      return false;
    }
    if (!Arrays.equals(_values, other._values)) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    return "Row[headerDefinition=" + _headerDefinition + ", values=" + Arrays.toString(_values) + "]";
  }

} public final class SearchRequest implements Request {
  private static final String EXTENSION = ".json";
  private static final String DATASETS_RELATIVE_URL = "datasets";
  private static final String DATABASE_CODE_PARAM = "database_code";
  private static final String QUERY_PARAM = "query";
  private static final String PAGE_PARAM = "page";
  private static final String PER_PAGE_PARAM = "per_page";

  private final String _databaseCode;
  private final String _query;
  private Integer _pageNumber;
  private Integer _maxDocsPerPage;

  private SearchRequest(final Builder builder) {
    _databaseCode = builder._databaseCode;
    _query = builder._query;
    _pageNumber = builder._pageNumber;
    _maxDocsPerPage = builder._maxDocsPerPage;
  }

  /**
   * Builder for this class. Examples:
   * 
   * <pre>
   * new SearchRequest.Builder().withQuery("query").withPageNumber(5).withMaxPerPage(100).build();
   * new SearchRequest.Builder().withQuery("query").build();
   * new SearchRequest.Builder().withDatabaseCode("databasecode").build();
   * </pre>
   */
  public static final class Builder {
    private String _databaseCode;
    private String _query;
    private Integer _pageNumber;
    private Integer _maxDocsPerPage;

    /**
     * Create an uninitialised Builder object.
     * @deprecated Prefer ofDatabaseCode or ofQuery static factory methods.
     */
    public Builder() {
    }

    @Deprecated
    private Builder(final String query) {
      _query = query;
    }

    /**
     * Factory method to create a meta data request instance. Use empty string to get all documents.
     * 
     * @param query an arbitrary query string, not null (note: can be empty string to get all documents)
     * @return an instance of this Builder for the given query string.
     * @deprecated use Builder().withQuery(query) instead
     */
    @Deprecated
    public static Builder of(final String query) {
      ArgumentChecker.notNull(query, "query");
      return new Builder(query);
    }
    
    /**
     * Create a builder and specify the database to search within. For example WIKI
     * 
     * @param databaseCode the database code to search within, not null
     * @return a Builder, with this database code specified
     */
    public static Builder ofDatabaseCode(final String databaseCode) {
      ArgumentChecker.notNull(databaseCode, "databaseCode");
      Builder builder = new Builder();
      builder._databaseCode = databaseCode;
      return builder;
    }

    /**
     * Specify the database to search within. For example WIKI
     * 
     * @param databaseCode the database code to search within, not null.
     * @return this Builder, with this database code specified
     */
    public Builder withDatabaseCode(final String databaseCode) {
      ArgumentChecker.notNull(databaseCode, "databaseCode");
      _databaseCode = databaseCode;
      return this;
    }
    
    /**
     * Create a builder set to search for all.
     * @return a Builder, set to search for all
     */
    public static Builder ofAll() {
      return new Builder();
    }

    /**
     * Specify the search term to use for your query. Multiple search terms can be separated by
     * the + character.
     * 
     * @param query the query string to use, not null
     * @return this Builder, with this query added
     */
    public Builder withQuery(final String query) {
      ArgumentChecker.notNull(query, "query");
      _query = query;
      return this;
    }
    
    /**
     * Create a builder and specify the search term to use for your query. Multiple search terms can be separated by
     * the + character.
     * 
     * @param query the query string to use, not null
     * @return a Builder, with this query added
     */
    public static Builder ofQuery(final String query) {
      ArgumentChecker.notNull(query, "query");
      Builder builder = new Builder();
      builder._query = query;
      return builder;
    }

    /**
     * Specify the page number of the results (1-based).
     * 
     * @param pageNumber the page number of the results, 1-based.
     * @return this Builder, with the page number information added
     */
    public Builder withPageNumber(final int pageNumber) {
      _pageNumber = pageNumber;
      return this;
    }

    /**
     * Specify the maximum number of documents per page of results. Currently, this is 
     * limited to 100, but this library does not enforce that in case the limit changes.
     * 
     * @param maxDocsPerPage the maximum number of documents per page (currently limited
     *                       to 100 by API)
     * @return this Builder, with the page number information added
     */
    public Builder withMaxPerPage(final int maxDocsPerPage) {
      _maxDocsPerPage = maxDocsPerPage;
      return this;
    }

    /**
     * Build an instance of the underlying request object.
     * 
     * @return an instance of the seach request object
     */
    public SearchRequest build() {
      return new SearchRequest(this);
    }
  }

  /**
   * @return the database code, or null if not set
   */
  public String getDatabaseCode() {
    return _databaseCode;
  }

  /**
   * @return the query string, or null if not set
   */
  public String getQuery() {
    return _query;
  }

  /**
   * @return the page number, or null if not set
   */
  public Integer getPageNumber() {
    return _pageNumber;
  }

  /**
   * @return the maximum number of results to return per page
   */
  public Integer getMaxPerPage() {
    return _maxDocsPerPage;
  }

  /**
   * Append any specified parameters to the provided WebTarget.
   * 
   * @param webTarget a web target used by the Jersey Client API, not null
   * @return the WebTarget with any path and query parameters appended
   */
  public WebTarget appendPathAndQueryParameters(final WebTarget webTarget) {
    ArgumentChecker.notNull(webTarget, "webTarget");
    WebTarget resultTarget = webTarget;
    resultTarget = resultTarget.path(DATASETS_RELATIVE_URL + EXTENSION);
    if (_databaseCode != null) {
      resultTarget = resultTarget.queryParam(DATABASE_CODE_PARAM, _databaseCode);
    }
    if (_query != null) {
      resultTarget = resultTarget.queryParam(QUERY_PARAM, _query);
    }
    if (_pageNumber != null) {
      resultTarget = resultTarget.queryParam(PAGE_PARAM, _pageNumber);
    }
    if (_maxDocsPerPage != null) {
      resultTarget = resultTarget.queryParam(PER_PAGE_PARAM, _maxDocsPerPage);
    }
    return resultTarget;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    if (_maxDocsPerPage != null) {
      result = prime * result + _maxDocsPerPage;
    }
    if (_pageNumber != null) {
      result = prime * result + _pageNumber;
    }
    if (_query != null) {
      result = prime * result + _query.hashCode();
    }
    if (_databaseCode != null) {
      result = prime * result + _databaseCode.hashCode();
    }
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof SearchRequest)) {
      return false;
    }
    SearchRequest other = (SearchRequest) obj;
    if (!_databaseCode.equals(other._databaseCode)) {
      return false;
    }
    if (!_query.equals(other._query)) {
      return false;
    }
    if (!_maxDocsPerPage.equals(other._maxDocsPerPage)) {
      return false;
    }
    if (!_pageNumber.equals(other._pageNumber)) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("SearchRequest[");
    if (_databaseCode != null) {
      builder.append("databaseCode=");
      builder.append(_databaseCode);
    }
    if (_query != null) {
      if (builder.charAt(builder.length() - 1) != '[') {
        builder.append(", ");
      }
      builder.append("query=");
      builder.append(_query);
    }
    if (_pageNumber != null) {
      if (builder.charAt(builder.length() - 1) != '[') {
        builder.append(", ");
      }
      builder.append("pageNumber=");
      builder.append(_pageNumber);
    }
    if (_maxDocsPerPage != null) {
      if (builder.charAt(builder.length() - 1) != '[') {
        builder.append(", ");
      }
      builder.append("maxDocsPerPage=");
      builder.append(_maxDocsPerPage);
    }
    builder.append("]");
    return builder.toString();
  }

  /**
   * Accept a request processor in visitor pattern style.
   * @param <T> the processor result type
   * @param processor  the request processor
   * @return the request processor's result
   */
  public <T> T accept(final RequestProcessor<T> processor) {
    return processor.processSearchRequest(this);
  }
} 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 final class TabularResult implements Iterable<Row> {
  /** definition of each column. */
  private HeaderDefinition _headerDefinition;
  /** list of rows. */
  private List<Row> _rows;

  /**
   * Private constructor, use static factory {@code of()}.
   * 
   * @param headerDefinition a list of the column headers
   * @param rows a list of rows
   */
  private TabularResult(final HeaderDefinition headerDefinition, final List<Row> rows) {
    _headerDefinition = headerDefinition;
    _rows = Collections.unmodifiableList(rows);
  }

  /**
   * Create a tabular result set from a header definition and an ordered list of rows.
   * 
   * @param headerDefinition the definition of the header row, not null
   * @param rows a list of rows, not null
   * @return the {@code TabularResult} instance
   */
  public static TabularResult of(final HeaderDefinition headerDefinition, final List<Row> rows) {
    ArgumentChecker.notNull(headerDefinition, "headerDefinition");
    ArgumentChecker.notNull(rows, "rows");
    return new TabularResult(headerDefinition, rows);
  }

  /**
   * Get the header definition.
   * 
   * @return the header definition, not null
   */
  public HeaderDefinition getHeaderDefinition() {
    return _headerDefinition;
  }

  /**
   * Get a row, indexed from zero (excluding the header). Throws an {@code IndexOutOfBoundsException} {@literal if index >= size()}
   * 
   * @param index the index of the row
   * @return the row, not null
   */
  public Row get(final int index) {
    return _rows.get(index);
  }

  /**
   * Get the number of rows in this {@code TabularResult}.
   * 
   * @return the number of rows
   */
  public int size() {
    return _rows.size();
  }

  /**
   * Return whether the result set empty.
   * 
   * @return true, if the result set is empty
   */
  public boolean isEmpty() {
    return _rows.isEmpty();
  }

  /**
   * Gets a row iterator.
   * 
   * @return an iterator over the rows
   */
  public Iterator<Row> iterator() {
    return _rows.iterator();
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + _headerDefinition.hashCode();
    result = prime * result + _rows.hashCode();
    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (!(obj instanceof TabularResult)) {
      return false;
    }
    TabularResult other = (TabularResult) obj;
    if (!_headerDefinition.equals(other._headerDefinition)) {
      return false;
    }
    if (!_rows.equals(other._rows)) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("TabularResult[headerDefinition=");
    builder.append(_headerDefinition);
    builder.append(", rows=");
    builder.append(_rows);
    builder.append("]");
    return builder.toString();
  }

  /**
   * Print a nicely formatted representation of this object. Currently prints a nicely formatted ACSII table.
   * 
   * @return a string containing a multi-line description of this object
   */
  public String toPrettyPrintedString() {
    return PrettyPrinter.toPrettyPrintedString(this);
  }
} public enum Transform {
  /**
   * Leave data unchanged.
   */
  NONE("none"),
  /**
   * Row-on-row change. y'[t] = y[t] - y[t-1]
   */
  DIFF("diff"),
  /**
   * Row-on-row % change. y'[t] = (y[t] - y[y-1]) / y[t-1]
   */
  RDIFF("rdiff"),
  /**
   * Cumulative sum. y'[t] = y[t] + y[t-1] + ... + y[0]
   */
  CUMUL("cumul"),
  /**
   * Start at 100. y'[t] = (y[t] / y[0]) * 100
   */
  NORMALIZE("normalize");

  private String _quandlString;

  /**
   * Create a transform enum with the appropriate quandl REST parameter string embedded.
   * 
   * @param quandlString the Quandl REST API string
   */
  Transform(final String quandlString) {
    _quandlString = quandlString;
  }

  /**
   * Get the Quandl REST API string for this enum.
   * 
   * @return the string expected by the Quandl REST API
   */
  String getQuandlString() {
    return _quandlString;
  }
} public class QuandlRuntimeException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  /**
   * Constructor when another exception is being included.
   * 
   * @param message a message describing the exception, not null
   * @param cause the cause of the expection if there is one, not null
   */
  public QuandlRuntimeException(final String message, final Throwable cause) {
    super(message, cause);
  }

  /**
   * Constructor when exception is not caused by an underlying exception.
   * 
   * @param message a message describing the exception, not null
   */
  public QuandlRuntimeException(final String message) {
    super(message);
  }
} public interface RESTDataProvider {
  /**
   * 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
   * @return the parsed JSON object
   */
  JSONObject getJSONResponse(WebTarget target);

  /**
   * 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
   * @return the parsed TabularResult
   */
  TabularResult getTabularResponse(WebTarget target);

}
import java.net.URI;
import java.util.Collections;
import javax.ws.rs.client.WebTarget;
import org.json.JSONObject;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.time.LocalDate;
import com.jimmoores.quandl.DataSetRequest;
import com.jimmoores.quandl.Frequency;
import com.jimmoores.quandl.HeaderDefinition;
import com.jimmoores.quandl.MetaDataRequest;
import com.jimmoores.quandl.MetaDataResult;
import com.jimmoores.quandl.MultiDataSetRequest;
import com.jimmoores.quandl.MultiMetaDataRequest;
import com.jimmoores.quandl.QuandlSession;
import com.jimmoores.quandl.Row;
import com.jimmoores.quandl.SearchRequest;
import com.jimmoores.quandl.SearchResult;
import com.jimmoores.quandl.SessionOptions;
import com.jimmoores.quandl.TabularResult;
import com.jimmoores.quandl.Transform;
import com.jimmoores.quandl.util.QuandlRuntimeException;
import com.jimmoores.quandl.util.RESTDataProvider;
package com.jimmoores.quandl.tests;





/**
 * @deprecated this test covers the deprecated API.
 * Tests to test the generation of URLs.
 */
@Test(groups = { "unit" })
public class URLGenerationTests {
  
  private static final int CLOSE_COLUMN = 4;
  @SuppressWarnings("unused")
  private static final int PRICE_COLUMN = 1;
  private static final HeaderDefinition TEST_HEADER_DEFINITION = HeaderDefinition.of("Date", "Close");
  private static final TabularResult TEST_TABULAR_RESULT = 
      TabularResult.of(TEST_HEADER_DEFINITION, Collections.singletonList(Row.of(TEST_HEADER_DEFINITION, new String[] { "Value1", "Value2"})));
  
  // CHECKSTYLE:OFF
  
  private class TestRESTDataProvider implements RESTDataProvider {

    private String _expectedURL;

    public TestRESTDataProvider(final String expectedURL) {
      _expectedURL = expectedURL;
    }
    
    public JSONObject getJSONResponse(final WebTarget target) {
      URI uri = target.getUriBuilder().build();
      Assert.assertEquals(_expectedURL, uri.toString());
      return new JSONObject();
    }

    public TabularResult getTabularResponse(final WebTarget target) {
      URI uri = target.getUriBuilder().build();
      Assert.assertEquals(_expectedURL, uri.toString());
      return TEST_TABULAR_RESULT;
    }
  }
  
  private QuandlSession getTestSession(final String expectedURL) {
    return QuandlSession.create(
             SessionOptions.Builder.withoutAuthToken()
                                   .withRESTDataProvider(
                                       new TestRESTDataProvider(expectedURL))
                                   .build());
  }

  @Test
  public void testSimpleGetDataSet() {
    QuandlSession session = getTestSession("https://www.quandl.com/api/v3/datasets/WIKI/MSFT.csv");
    TabularResult tabularResult = session.getDataSet(DataSetRequest.Builder.of("WIKI/MSFT").build());
    Assert.assertEquals(TEST_TABULAR_RESULT, tabularResult);
  }
  
  @Test
  public void testMoreComplexGetDataSet() {
    QuandlSession session = getTestSession("https://www.quandl.com/api/v3/datasets/WIKI/AAPL.csv?column_index=4&collapse=quarterly&transform=normalize");
    TabularResult tabularResult = session.getDataSet(DataSetRequest.Builder.of("WIKI/AAPL")
                                                                           .withFrequency(Frequency.QUARTERLY)