[DRILL-8457] Allow configuring csv parser in http storage plugin configuration (#2840)
diff --git a/contrib/storage-http/CSV_Options.md b/contrib/storage-http/CSV_Options.md
new file mode 100644
index 0000000..43ebb79
--- /dev/null
+++ b/contrib/storage-http/CSV_Options.md
@@ -0,0 +1,139 @@
+# CSV options and configuration
+
+CSV parser of HTTP Storage plugin can be configured using `csvOptions`.
+
+```json
+{
+ "csvOptions": {
+ "delimiter": ",",
+ "quote": "\"",
+ "quoteEscape": "\"",
+ "lineSeparator": "\n",
+ "headerExtractionEnabled": null,
+ "numberOfRowsToSkip": 0,
+ "numberOfRecordsToRead": -1,
+ "lineSeparatorDetectionEnabled": true,
+ "maxColumns": 512,
+ "maxCharsPerColumn": 4096,
+ "skipEmptyLines": true,
+ "ignoreLeadingWhitespaces": true,
+ "ignoreTrailingWhitespaces": true,
+ "nullValue": null
+ }
+}
+```
+
+## Configuration options
+
+- **delimiter**: The character used to separate individual values in a CSV record.
+ Default: `,`
+
+- **quote**: The character used to enclose fields that may contain special characters (like the
+ delimiter or line separator).
+ Default: `"`
+
+- **quoteEscape**: The character used to escape a quote inside a field enclosed by quotes.
+ Default: `"`
+
+- **lineSeparator**: The string that represents a line break in the CSV file.
+ Default: `\n`
+
+- **headerExtractionEnabled**: Determines if the first row of the CSV contains the headers (field
+ names). If set to `true`, the parser will use the first row as headers.
+ Default: `null`
+
+- **numberOfRowsToSkip**: Number of rows to skip before starting to read records. Useful for
+ skipping initial lines that are not records or headers.
+ Default: `0`
+
+- **numberOfRecordsToRead**: Specifies the maximum number of records to read from the input. A
+ negative value (e.g., `-1`) means there's no limit.
+ Default: `-1`
+
+- **lineSeparatorDetectionEnabled**: When set to `true`, the parser will automatically detect and
+ use the line separator present in the input. This is useful when you don't know the line separator
+ in advance.
+ Default: `true`
+
+- **maxColumns**: The maximum number of columns a record can have. Any record with more columns than
+ this will cause an exception.
+ Default: `512`
+
+- **maxCharsPerColumn**: The maximum number of characters a single field can have. Any field with
+ more characters than this will cause an exception.
+ Default: `4096`
+
+- **skipEmptyLines**: When set to `true`, the parser will skip any lines that are empty or only
+ contain whitespace.
+ Default: `true`
+
+- **ignoreLeadingWhitespaces**: When set to `true`, the parser will ignore any whitespaces at the
+ start of a field.
+ Default: `true`
+
+- **ignoreTrailingWhitespaces**: When set to `true`, the parser will ignore any whitespaces at the
+ end of a field.
+ Default: `true`
+
+- **nullValue**: Specifies a string that should be interpreted as a `null` value when reading. If a
+ field matches this string, it will be returned as `null`.
+ Default: `null`
+
+## Example
+
+### Parse tsv
+
+To parse `.tsv` files you can use a following `csvOptions` config:
+
+```json
+{
+ "csvOptions": {
+ "delimiter": "\t"
+ }
+}
+```
+
+Then we can create a following connector plugin which queries a `.tsv` file from GitHub, let's call
+it `github`:
+
+```json
+{
+ "type": "http",
+ "connections": {
+ "test-data": {
+ "url": "https://raw.githubusercontent.com/semantic-web-company/wic-tsv/master/data/de/Test/test_examples.txt",
+ "requireTail": false,
+ "method": "GET",
+ "authType": "none",
+ "inputType": "csv",
+ "xmlDataLevel": 1,
+ "postParameterLocation": "QUERY_STRING",
+ "csvOptions": {
+ "delimiter": "\t",
+ "quote": "\"",
+ "quoteEscape": "\"",
+ "lineSeparator": "\n",
+ "numberOfRecordsToRead": -1,
+ "lineSeparatorDetectionEnabled": true,
+ "maxColumns": 512,
+ "maxCharsPerColumn": 4096,
+ "skipEmptyLines": true,
+ "ignoreLeadingWhitespaces": true,
+ "ignoreTrailingWhitespaces": true
+ },
+ "verifySSLCert": true
+ }
+ },
+ "timeout": 5,
+ "retryDelay": 1000,
+ "proxyType": "direct",
+ "authMode": "SHARED_USER",
+ "enabled": true
+}
+```
+
+And we can query it using a following query:
+
+```sql
+SELECT * from github.`test-data`
+```
diff --git a/contrib/storage-http/README.md b/contrib/storage-http/README.md
index a19017e..05f4102 100644
--- a/contrib/storage-http/README.md
+++ b/contrib/storage-http/README.md
@@ -294,6 +294,9 @@
#### JSON Configuration
[Read the documentation for configuring json options, including schema provisioning.](JSON_Options.md)
+#### CSV Configuration
+[Read the documentation for configuring csv options.](CSV_Options.md)
+
#### XML Configuration
[Read the documentation for configuring XML options, including schema provisioning.](XML_Options.md)
diff --git a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpApiConfig.java b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpApiConfig.java
index 43bc230..6511320 100644
--- a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpApiConfig.java
+++ b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpApiConfig.java
@@ -22,6 +22,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.common.collect.ImmutableList;
import okhttp3.HttpUrl;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -31,7 +32,6 @@
import org.apache.drill.exec.store.security.CredentialProviderUtils;
import org.apache.drill.exec.store.security.UsernamePasswordCredentials;
import org.apache.drill.exec.store.security.UsernamePasswordWithProxyCredentials;
-import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +40,7 @@
import java.util.Objects;
import java.util.Optional;
+
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@JsonDeserialize(builder = HttpApiConfig.HttpApiConfigBuilder.class)
public class HttpApiConfig {
@@ -116,6 +117,9 @@
@JsonProperty
private final HttpXmlOptions xmlOptions;
+ @JsonProperty
+ private final HttpCSVOptions csvOptions;
+
@JsonInclude
@JsonProperty
private final boolean verifySSLCert;
@@ -185,10 +189,15 @@
public HttpJsonOptions jsonOptions() {
return this.jsonOptions;
}
+
public HttpXmlOptions xmlOptions() {
return this.xmlOptions;
}
+ public HttpCSVOptions csvOptions() {
+ return this.csvOptions;
+ }
+
public boolean verifySSLCert() {
return this.verifySSLCert;
}
@@ -211,56 +220,59 @@
}
HttpApiConfig that = (HttpApiConfig) o;
return requireTail == that.requireTail
- && errorOn400 == that.errorOn400
- && verifySSLCert == that.verifySSLCert
- && directCredentials == that.directCredentials
- && caseSensitiveFilters == that.caseSensitiveFilters
- && Objects.equals(url, that.url)
- && Objects.equals(method, that.method)
- && Objects.equals(postBody, that.postBody)
- && Objects.equals(headers, that.headers)
- && Objects.equals(params, that.params)
- && Objects.equals(postParameterLocation, that.postParameterLocation)
- && Objects.equals(dataPath, that.dataPath)
- && Objects.equals(authType, that.authType)
- && Objects.equals(inputType, that.inputType)
- && Objects.equals(limitQueryParam, that.limitQueryParam)
- && Objects.equals(jsonOptions, that.jsonOptions)
- && Objects.equals(xmlOptions, that.xmlOptions)
- && Objects.equals(credentialsProvider, that.credentialsProvider)
- && Objects.equals(paginator, that.paginator);
+ && errorOn400 == that.errorOn400
+ && verifySSLCert == that.verifySSLCert
+ && directCredentials == that.directCredentials
+ && caseSensitiveFilters == that.caseSensitiveFilters
+ && Objects.equals(url, that.url)
+ && Objects.equals(method, that.method)
+ && Objects.equals(postBody, that.postBody)
+ && Objects.equals(headers, that.headers)
+ && Objects.equals(params, that.params)
+ && Objects.equals(postParameterLocation, that.postParameterLocation)
+ && Objects.equals(dataPath, that.dataPath)
+ && Objects.equals(authType, that.authType)
+ && Objects.equals(inputType, that.inputType)
+ && Objects.equals(limitQueryParam, that.limitQueryParam)
+ && Objects.equals(jsonOptions, that.jsonOptions)
+ && Objects.equals(xmlOptions, that.xmlOptions)
+ && Objects.equals(credentialsProvider, that.credentialsProvider)
+ && Objects.equals(paginator, that.paginator)
+ && Objects.equals(csvOptions, that.csvOptions);
}
@Override
public int hashCode() {
return Objects.hash(url, requireTail, method, postBody, headers, params, dataPath,
- authType, inputType, limitQueryParam, errorOn400, jsonOptions, xmlOptions, verifySSLCert,
- credentialsProvider, paginator, directCredentials, postParameterLocation, caseSensitiveFilters);
+ authType, inputType, limitQueryParam, errorOn400, jsonOptions, xmlOptions, verifySSLCert,
+ credentialsProvider, paginator, directCredentials, postParameterLocation,
+ caseSensitiveFilters, csvOptions);
}
@Override
public String toString() {
return new PlanStringBuilder(this)
- .field("url", url)
- .field("requireTail", requireTail)
- .field("method", method)
- .field("postBody", postBody)
- .field("postParameterLocation", postParameterLocation)
- .field("headers", headers)
- .field("params", params)
- .field("dataPath", dataPath)
- .field("caseSensitiveFilters", caseSensitiveFilters)
- .field("authType", authType)
- .field("inputType", inputType)
- .field("limitQueryParam", limitQueryParam)
- .field("errorOn400", errorOn400)
- .field("jsonOptions", jsonOptions)
- .field("xmlOptions", xmlOptions)
- .field("verifySSLCert", verifySSLCert)
- .field("credentialsProvider", credentialsProvider)
- .field("paginator", paginator)
- .field("directCredentials", directCredentials)
- .toString();
+ .field("url", url)
+ .field("requireTail", requireTail)
+ .field("method", method)
+ .field("postBody", postBody)
+ .field("postParameterLocation", postParameterLocation)
+ .field("headers", headers)
+ .field("params", params)
+ .field("dataPath", dataPath)
+ .field("caseSensitiveFilters", caseSensitiveFilters)
+ .field("authType", authType)
+ .field("inputType", inputType)
+ .field("limitQueryParam", limitQueryParam)
+ .field("errorOn400", errorOn400)
+ .field("jsonOptions", jsonOptions)
+ .field("xmlOptions", xmlOptions)
+ .field("verifySSLCert", verifySSLCert)
+ .field("credentialsProvider", credentialsProvider)
+ .field("paginator", paginator)
+ .field("directCredentials", directCredentials)
+ .field("csvOptions", csvOptions)
+ .toString();
}
/**
@@ -307,39 +319,50 @@
this.url = builder.url;
this.jsonOptions = builder.jsonOptions;
this.xmlOptions = builder.xmlOptions;
+ this.csvOptions = builder.csvOptions;
- HttpMethod httpMethod = HttpMethod.valueOf(this.method);
- // Get the request method. Only accept GET and POST requests. Anything else will default to GET.
+ final HttpMethod httpMethod;
+ try {
+ httpMethod = HttpMethod.valueOf(this.method);
+ } catch (IllegalArgumentException e) {
+ throw UserException
+ .validationError()
+ .message("Invalid HTTP method: %s. Drill supports 'GET' and , 'POST'.", method)
+ .build(logger);
+ }
+
+ // Get the request method. Only accept GET and POST requests. Anything else will default to
+ // GET.
switch (httpMethod) {
- case GET:
- case POST:
- break;
- default:
- throw UserException
+ case GET:
+ case POST:
+ break;
+ default:
+ throw UserException
.validationError()
.message("Invalid HTTP method: %s. Drill supports 'GET' and , 'POST'.", method)
.build(logger);
}
if (StringUtils.isEmpty(url)) {
throw UserException
- .validationError()
- .message("URL is required for the HTTP storage plugin.")
- .build(logger);
+ .validationError()
+ .message("URL is required for the HTTP storage plugin.")
+ .build(logger);
}
// Default to query string to avoid breaking changes
this.postParameterLocation = StringUtils.isEmpty(builder.postParameterLocation) ?
- PostLocation.QUERY_STRING.toString() : builder.postParameterLocation.trim().toUpperCase();
+ PostLocation.QUERY_STRING.toString() : builder.postParameterLocation.trim().toUpperCase();
- // Get the authentication method. Future functionality will include OAUTH2 authentication but for now
+ // Get the authentication method. Future functionality will include OAUTH2 authentication but
+ // for now
// Accept either basic or none. The default is none.
this.authType = StringUtils.defaultIfEmpty(builder.authType, "none");
this.postBody = builder.postBody;
-
this.params = CollectionUtils.isEmpty(builder.params) ? null :
- ImmutableList.copyOf(builder.params);
+ ImmutableList.copyOf(builder.params);
this.dataPath = StringUtils.defaultIfEmpty(builder.dataPath, null);
// Default to true for backward compatibility with first PR.
@@ -353,7 +376,8 @@
this.xmlDataLevel = Math.max(1, builder.xmlDataLevel);
this.errorOn400 = builder.errorOn400;
this.caseSensitiveFilters = builder.caseSensitiveFilters;
- this.credentialsProvider = CredentialProviderUtils.getCredentialsProvider(builder.userName, builder.password, builder.credentialsProvider);
+ this.credentialsProvider = CredentialProviderUtils.getCredentialsProvider(builder.userName,
+ builder.password, builder.credentialsProvider);
this.directCredentials = builder.credentialsProvider == null;
this.limitQueryParam = builder.limitQueryParam;
@@ -366,8 +390,8 @@
return null;
}
return getUsernamePasswordCredentials()
- .map(UsernamePasswordCredentials::getUsername)
- .orElse(null);
+ .map(UsernamePasswordCredentials::getUsername)
+ .orElse(null);
}
@JsonProperty
@@ -376,8 +400,8 @@
return null;
}
return getUsernamePasswordCredentials()
- .map(UsernamePasswordCredentials::getPassword)
- .orElse(null);
+ .map(UsernamePasswordCredentials::getPassword)
+ .orElse(null);
}
@JsonIgnore
@@ -398,16 +422,16 @@
@JsonIgnore
public Optional<UsernamePasswordWithProxyCredentials> getUsernamePasswordCredentials() {
return new UsernamePasswordWithProxyCredentials.Builder()
- .setCredentialsProvider(credentialsProvider)
- .build();
+ .setCredentialsProvider(credentialsProvider)
+ .build();
}
@JsonIgnore
public Optional<UsernamePasswordWithProxyCredentials> getUsernamePasswordCredentials(String username) {
return new UsernamePasswordWithProxyCredentials.Builder()
- .setCredentialsProvider(credentialsProvider)
- .setQueryUser(username)
- .build();
+ .setCredentialsProvider(credentialsProvider)
+ .setQueryUser(username)
+ .build();
}
@JsonProperty
@@ -455,6 +479,8 @@
private HttpJsonOptions jsonOptions;
private HttpXmlOptions xmlOptions;
+ private HttpCSVOptions csvOptions;
+
private CredentialsProvider credentialsProvider;
private HttpPaginatorConfig paginator;
@@ -481,6 +507,10 @@
return this.verifySSLCert;
}
+ public HttpCSVOptions csvOptions() {
+ return this.csvOptions;
+ }
+
public String inputType() {
return this.inputType;
}
@@ -562,6 +592,7 @@
/**
* Do not use. Use xmlOptions instead to set XML data level.
+ *
* @param xmlDataLevel
* @return
*/
@@ -586,6 +617,11 @@
return this;
}
+ public HttpApiConfigBuilder csvOptions(HttpCSVOptions options) {
+ this.csvOptions = options;
+ return this;
+ }
+
public HttpApiConfigBuilder credentialsProvider(CredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
return this;
diff --git a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVBatchReader.java b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVBatchReader.java
index f12cbfa..1428d96 100644
--- a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVBatchReader.java
+++ b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVBatchReader.java
@@ -18,6 +18,7 @@
package org.apache.drill.exec.store.http;
+import com.univocity.parsers.csv.CsvFormat;
import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings;
import okhttp3.HttpUrl;
@@ -44,10 +45,10 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
public class HttpCSVBatchReader extends HttpBatchReader {
private final HttpSubScan subScan;
- private final CsvParserSettings csvSettings;
private final int maxRecords;
private CsvParser csvReader;
private List<StringColumnWriter> columnWriters;
@@ -63,18 +64,43 @@
super(subScan);
this.subScan = subScan;
this.maxRecords = subScan.maxRecords();
-
- this.csvSettings = new CsvParserSettings();
- csvSettings.setLineSeparatorDetectionEnabled(true);
}
public HttpCSVBatchReader(HttpSubScan subScan, Paginator paginator) {
super(subScan, paginator);
this.subScan = subScan;
this.maxRecords = subScan.maxRecords();
+ }
- this.csvSettings = new CsvParserSettings();
- csvSettings.setLineSeparatorDetectionEnabled(true);
+ private CsvParserSettings buildCsvSettings() {
+ CsvParserSettings settings = new CsvParserSettings();
+ CsvFormat format = settings.getFormat();
+ HttpCSVOptions csvOptions = subScan.tableSpec().connectionConfig().csvOptions();
+
+ if (Objects.isNull(csvOptions)) {
+ settings.setLineSeparatorDetectionEnabled(true);
+ return settings;
+ }
+
+ format.setDelimiter(csvOptions.getDelimiter());
+ format.setQuote(csvOptions.getQuote());
+ format.setQuoteEscape(csvOptions.getQuoteEscape());
+ format.setLineSeparator(csvOptions.getLineSeparator());
+
+ settings.setLineSeparatorDetectionEnabled(csvOptions.isLineSeparatorDetectionEnabled());
+ if (!Objects.isNull(csvOptions.getHeaderExtractionEnabled())) {
+ settings.setHeaderExtractionEnabled(csvOptions.getHeaderExtractionEnabled());
+ }
+ settings.setNullValue(csvOptions.getNullValue());
+ settings.setNumberOfRowsToSkip(csvOptions.getNumberOfRowsToSkip());
+ settings.setNumberOfRecordsToRead(csvOptions.getNumberOfRecordsToRead());
+ settings.setMaxColumns(csvOptions.getMaxColumns());
+ settings.setMaxCharsPerColumn(csvOptions.getMaxCharsPerColumn());
+ settings.setSkipEmptyLines(csvOptions.isSkipEmptyLines());
+ settings.setIgnoreLeadingWhitespaces(csvOptions.isIgnoreLeadingWhitespaces());
+ settings.setIgnoreTrailingWhitespaces(csvOptions.isIgnoreTrailingWhitespaces());
+
+ return settings;
}
@Override
@@ -96,17 +122,18 @@
// Http client setup
SimpleHttp http = SimpleHttp.builder()
- .scanDefn(subScan)
- .url(url)
- .tempDir(new File(tempDirPath))
- .paginator(paginator)
- .proxyConfig(proxySettings(negotiator.drillConfig(), url))
- .errorContext(errorContext)
- .build();
+ .scanDefn(subScan)
+ .url(url)
+ .tempDir(new File(tempDirPath))
+ .paginator(paginator)
+ .proxyConfig(proxySettings(negotiator.drillConfig(), url))
+ .errorContext(errorContext)
+ .build();
// CSV loader setup
inStream = http.getInputStream();
+ CsvParserSettings csvSettings = buildCsvSettings();
this.csvReader = new CsvParser(csvSettings);
csvReader.beginParsing(inStream);
@@ -181,7 +208,7 @@
if (nextRow == null) {
if (paginator != null &&
- maxRecords < 0 && (resultLoader.totalRowCount()) < paginator.getPageSize()) {
+ maxRecords < 0 && (resultLoader.totalRowCount()) < paginator.getPageSize()) {
paginator.notifyPartialPage();
}
return false;
@@ -211,7 +238,8 @@
this.columnIndex = columnIndex;
}
- public void load(String[] record) {}
+ public void load(String[] record) {
+ }
}
public static class StringColumnWriter extends ColumnWriter {
diff --git a/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVOptions.java b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVOptions.java
new file mode 100644
index 0000000..c8d5da1
--- /dev/null
+++ b/contrib/storage-http/src/main/java/org/apache/drill/exec/store/http/HttpCSVOptions.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import org.apache.drill.common.PlanStringBuilder;
+
+import java.util.Objects;
+
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+@JsonDeserialize(builder = HttpCSVOptions.HttpCSVOptionsBuilder.class)
+public class HttpCSVOptions {
+
+
+ @JsonProperty
+ private final String delimiter;
+
+ @JsonProperty
+ private final char quote;
+
+ @JsonProperty
+ private final char quoteEscape;
+
+ @JsonProperty
+ private final String lineSeparator;
+
+ @JsonProperty
+ private final Boolean headerExtractionEnabled;
+
+ @JsonProperty
+ private final long numberOfRowsToSkip;
+
+ @JsonProperty
+ private final long numberOfRecordsToRead;
+
+ @JsonProperty
+ private final boolean lineSeparatorDetectionEnabled;
+
+ @JsonProperty
+ private final int maxColumns;
+
+ @JsonProperty
+ private final int maxCharsPerColumn;
+
+ @JsonProperty
+ private final boolean skipEmptyLines;
+
+ @JsonProperty
+ private final boolean ignoreLeadingWhitespaces;
+
+ @JsonProperty
+ private final boolean ignoreTrailingWhitespaces;
+
+ @JsonProperty
+ private final String nullValue;
+
+ HttpCSVOptions(HttpCSVOptionsBuilder builder) {
+ this.delimiter = builder.delimiter;
+ this.quote = builder.quote;
+ this.quoteEscape = builder.quoteEscape;
+ this.lineSeparator = builder.lineSeparator;
+ this.headerExtractionEnabled = builder.headerExtractionEnabled;
+ this.numberOfRowsToSkip = builder.numberOfRowsToSkip;
+ this.numberOfRecordsToRead = builder.numberOfRecordsToRead;
+ this.lineSeparatorDetectionEnabled = builder.lineSeparatorDetectionEnabled;
+ this.maxColumns = builder.maxColumns;
+ this.maxCharsPerColumn = builder.maxCharsPerColumn;
+ this.skipEmptyLines = builder.skipEmptyLines;
+ this.ignoreLeadingWhitespaces = builder.ignoreLeadingWhitespaces;
+ this.ignoreTrailingWhitespaces = builder.ignoreTrailingWhitespaces;
+ this.nullValue = builder.nullValue;
+ }
+
+ public static HttpCSVOptionsBuilder builder() {
+ return new HttpCSVOptionsBuilder();
+ }
+
+ public String getDelimiter() {
+ return delimiter;
+ }
+
+ public char getQuote() {
+ return quote;
+ }
+
+ public char getQuoteEscape() {
+ return quoteEscape;
+ }
+
+ public String getLineSeparator() {
+ return lineSeparator;
+ }
+
+ public Boolean getHeaderExtractionEnabled() {
+ return headerExtractionEnabled;
+ }
+
+ public long getNumberOfRowsToSkip() {
+ return numberOfRowsToSkip;
+ }
+
+ public long getNumberOfRecordsToRead() {
+ return numberOfRecordsToRead;
+ }
+
+ public boolean isLineSeparatorDetectionEnabled() {
+ return lineSeparatorDetectionEnabled;
+ }
+
+ public int getMaxColumns() {
+ return maxColumns;
+ }
+
+ public int getMaxCharsPerColumn() {
+ return maxCharsPerColumn;
+ }
+
+ public boolean isSkipEmptyLines() {
+ return skipEmptyLines;
+ }
+
+ public boolean isIgnoreLeadingWhitespaces() {
+ return ignoreLeadingWhitespaces;
+ }
+
+ public boolean isIgnoreTrailingWhitespaces() {
+ return ignoreTrailingWhitespaces;
+ }
+
+ public String getNullValue() {
+ return nullValue;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ HttpCSVOptions that = (HttpCSVOptions) o;
+ return quote == that.quote
+ && quoteEscape == that.quoteEscape
+ && numberOfRowsToSkip == that.numberOfRowsToSkip
+ && numberOfRecordsToRead == that.numberOfRecordsToRead
+ && lineSeparatorDetectionEnabled == that.lineSeparatorDetectionEnabled
+ && maxColumns == that.maxColumns && maxCharsPerColumn == that.maxCharsPerColumn
+ && skipEmptyLines == that.skipEmptyLines
+ && ignoreLeadingWhitespaces == that.ignoreLeadingWhitespaces
+ && ignoreTrailingWhitespaces == that.ignoreTrailingWhitespaces
+ && delimiter.equals(that.delimiter)
+ && lineSeparator.equals(that.lineSeparator)
+ && Objects.equals(headerExtractionEnabled, that.headerExtractionEnabled)
+ && nullValue.equals(that.nullValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(delimiter, quote, quoteEscape, lineSeparator, headerExtractionEnabled,
+ numberOfRowsToSkip, numberOfRecordsToRead, lineSeparatorDetectionEnabled, maxColumns,
+ maxCharsPerColumn, skipEmptyLines, ignoreLeadingWhitespaces, ignoreTrailingWhitespaces,
+ nullValue);
+ }
+
+ @Override
+ public String toString() {
+ return new PlanStringBuilder(this)
+ .field("delimiter", delimiter)
+ .field("quote", quote)
+ .field("quoteEscape", quoteEscape)
+ .field("lineSeparator", lineSeparator)
+ .field("headerExtractionEnabled", headerExtractionEnabled)
+ .field("numberOfRowsToSkip", numberOfRowsToSkip)
+ .field("numberOfRecordsToRead", numberOfRecordsToRead)
+ .field("lineSeparatorDetectionEnabled", lineSeparatorDetectionEnabled)
+ .field("maxColumns", maxColumns)
+ .field("maxCharsPerColumn", maxCharsPerColumn)
+ .field("skipEmptyLines", skipEmptyLines)
+ .field("ignoreLeadingWhitespaces", ignoreLeadingWhitespaces)
+ .field("ignoreTrailingWhitespaces", ignoreTrailingWhitespaces)
+ .field("nullValue", nullValue)
+ .toString();
+ }
+
+
+ @JsonPOJOBuilder(withPrefix = "")
+ public static class HttpCSVOptionsBuilder {
+ private String delimiter = ",";
+ private char quote = '"';
+
+ private char quoteEscape = '"';
+
+ private String lineSeparator = "\n";
+
+ private Boolean headerExtractionEnabled = null;
+
+ private long numberOfRowsToSkip = 0;
+
+ private long numberOfRecordsToRead = -1;
+
+ private boolean lineSeparatorDetectionEnabled = true;
+
+ private int maxColumns = 512;
+
+ private int maxCharsPerColumn = 4096;
+
+ private boolean skipEmptyLines = true;
+
+ private boolean ignoreLeadingWhitespaces = true;
+
+ private boolean ignoreTrailingWhitespaces = true;
+
+ private String nullValue = null;
+
+
+ public HttpCSVOptionsBuilder delimiter(String delimiter) {
+ this.delimiter = delimiter;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder quote(char quote) {
+ this.quote = quote;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder quoteEscape(char quoteEscape) {
+ this.quoteEscape = quoteEscape;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder lineSeparator(String lineSeparator) {
+ this.lineSeparator = lineSeparator;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder headerExtractionEnabled(Boolean headerExtractionEnabled) {
+ this.headerExtractionEnabled = headerExtractionEnabled;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder numberOfRowsToSkip(long numberOfRowsToSkip) {
+ this.numberOfRowsToSkip = numberOfRowsToSkip;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder numberOfRecordsToRead(long numberOfRecordsToRead) {
+ this.numberOfRecordsToRead = numberOfRecordsToRead;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder lineSeparatorDetectionEnabled(boolean lineSeparatorDetectionEnabled) {
+ this.lineSeparatorDetectionEnabled = lineSeparatorDetectionEnabled;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder maxColumns(int maxColumns) {
+ this.maxColumns = maxColumns;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder maxCharsPerColumn(int maxCharsPerColumn) {
+ this.maxCharsPerColumn = maxCharsPerColumn;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder skipEmptyLines(boolean skipEmptyLines) {
+ this.skipEmptyLines = skipEmptyLines;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder ignoreLeadingWhitespaces(boolean ignoreLeadingWhitespaces) {
+ this.ignoreLeadingWhitespaces = ignoreLeadingWhitespaces;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder ignoreTrailingWhitespaces(boolean ignoreTrailingWhitespaces) {
+ this.ignoreTrailingWhitespaces = ignoreTrailingWhitespaces;
+ return this;
+ }
+
+ public HttpCSVOptionsBuilder nullValue(String nullValue) {
+ this.nullValue = nullValue;
+ return this;
+ }
+
+
+ public HttpCSVOptions build() {
+
+ return new HttpCSVOptions(this);
+ }
+ }
+}
diff --git a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpApiConfig.java b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpApiConfig.java
new file mode 100644
index 0000000..35e5f3b
--- /dev/null
+++ b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpApiConfig.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.common.util.DrillFileUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class TestHttpApiConfig {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static String EXAMPLE_HTTP_API_CONFIG_JSON;
+
+ private static Map<String, String> EXAMPLE_HEADERS;
+
+ @BeforeAll
+ public static void setup() throws Exception {
+ EXAMPLE_HTTP_API_CONFIG_JSON = Files.asCharSource(
+ DrillFileUtils.getResourceAsFile("/data/exampleHttpApiConfig.json"), Charsets.UTF_8
+ ).read().trim();
+
+ EXAMPLE_HEADERS = new HashMap<>();
+ EXAMPLE_HEADERS.put("Authorization", "Bearer token");
+ }
+
+ @Test
+ public void testBuilderDefaults() {
+ HttpApiConfig config = HttpApiConfig.builder().url("http://example.com").build();
+
+ assertEquals("http://example.com", config.url());
+ assertEquals("GET", config.method());
+ assertTrue(config.verifySSLCert());
+ assertTrue(config.requireTail());
+ assertEquals(HttpApiConfig.DEFAULT_INPUT_FORMAT, config.inputType());
+ assertEquals("QUERY_STRING", config.getPostParameterLocation());
+ assertEquals("none", config.authType());
+
+ assertNull(config.postBody());
+ assertNull(config.headers());
+ assertNull(config.params());
+ assertNull(config.dataPath());
+ assertNull(config.jsonOptions());
+ assertNull(config.xmlOptions());
+ assertNull(config.csvOptions());
+ assertNull(config.paginator());
+ assertNull(config.userName());
+ assertNull(config.password());
+ assertNull(config.credentialsProvider());
+ }
+
+ @Test
+ public void testBuilder() {
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Authorization", "Bearer token");
+
+ HttpApiConfig.HttpApiConfigBuilder builder = HttpApiConfig.builder()
+ .url("http://example.com")
+ .method("GET")
+ .postBody("testBody")
+ .postParameterLocation(HttpApiConfig.POST_BODY_POST_LOCATION)
+ .headers(headers)
+ .params(Arrays.asList("param1", "param2"))
+ .dataPath("/data/path")
+ .authType("none")
+ .inputType("json")
+ .limitQueryParam("limit")
+ .errorOn400(true)
+ .jsonOptions(null)
+ .xmlOptions(null)
+ .csvOptions(null)
+ .paginator(null)
+ .requireTail(true)
+ .verifySSLCert(true)
+ .caseSensitiveFilters(true);
+
+ HttpApiConfig config = builder.build();
+
+ assertEquals("http://example.com", config.url());
+ assertEquals("GET", config.method());
+ assertEquals("testBody", config.postBody());
+ assertEquals("POST_BODY", config.getPostParameterLocation());
+ assertEquals(headers, config.headers());
+ assertEquals(Arrays.asList("param1", "param2"), config.params());
+ assertEquals("/data/path", config.dataPath());
+ assertEquals("none", config.authType());
+ assertEquals("json", config.inputType());
+ assertEquals("limit", config.limitQueryParam());
+ assertTrue(config.errorOn400());
+ assertNull(config.jsonOptions());
+ assertNull(config.xmlOptions());
+ assertNull(config.csvOptions());
+ assertNull(config.paginator());
+ assertTrue(config.verifySSLCert());
+ assertTrue(config.requireTail());
+ assertTrue(config.caseSensitiveFilters());
+ }
+
+ @Test
+ public void testUserExceptionOnNoURL() {
+ HttpApiConfig config = HttpApiConfig.builder().url("http://example.com").build();
+
+ assertEquals("http://example.com", config.url());
+ assertEquals("GET", config.method());
+ assertTrue(config.verifySSLCert());
+ assertTrue(config.requireTail());
+ assertEquals(HttpApiConfig.DEFAULT_INPUT_FORMAT, config.inputType());
+ assertEquals("QUERY_STRING", config.getPostParameterLocation());
+ assertEquals("none", config.authType());
+
+ assertNull(config.postBody());
+ assertNull(config.headers());
+ assertNull(config.params());
+ assertNull(config.dataPath());
+ assertNull(config.jsonOptions());
+ assertNull(config.xmlOptions());
+ assertNull(config.csvOptions());
+ assertNull(config.paginator());
+ assertNull(config.userName());
+ assertNull(config.password());
+ assertNull(config.credentialsProvider());
+ }
+
+ @Test
+ public void testInvalidHttpMethod() {
+ String invalidMethod = "INVALID";
+
+ assertThrows(UserException.class, () -> {
+ HttpApiConfig.builder()
+ .method(invalidMethod)
+ .build();
+ });
+ }
+
+ @Test
+ public void testErrorOnEmptyURL() {
+
+ assertThrows(UserException.class, () -> {
+ HttpApiConfig.builder()
+ .url(null)
+ .build();
+ });
+
+ assertThrows(UserException.class, () -> {
+ HttpApiConfig.builder()
+ .url("")
+ .build();
+ });
+ }
+
+ @Test
+ public void testJSONSerialization() throws JsonProcessingException {
+ HttpApiConfig.HttpApiConfigBuilder builder = HttpApiConfig.builder()
+ .url("http://example.com")
+ .method("GET")
+ .postBody("testBody")
+ .postParameterLocation(HttpApiConfig.POST_BODY_POST_LOCATION)
+ .headers(EXAMPLE_HEADERS)
+ .params(Arrays.asList("param1", "param2"))
+ .dataPath("/data/path")
+ .authType("none")
+ .inputType("json")
+ .limitQueryParam("limit")
+ .errorOn400(true)
+ .jsonOptions(null)
+ .xmlOptions(null)
+ .csvOptions(null)
+ .paginator(null)
+ .requireTail(true)
+ .verifySSLCert(true)
+ .caseSensitiveFilters(true);
+
+ HttpApiConfig config = builder.build();
+ String json = objectMapper.writeValueAsString(config);
+
+ assertEquals(EXAMPLE_HTTP_API_CONFIG_JSON, json);
+ }
+
+ @Test
+ public void testJSONDeserialization() throws JsonProcessingException {
+ HttpApiConfig config = objectMapper.readValue(EXAMPLE_HTTP_API_CONFIG_JSON,
+ HttpApiConfig.class);
+
+ assertEquals("http://example.com", config.url());
+ assertEquals("GET", config.method());
+ assertEquals("testBody", config.postBody());
+ assertEquals("POST_BODY", config.getPostParameterLocation());
+ assertEquals(EXAMPLE_HEADERS, config.headers());
+ assertEquals(Arrays.asList("param1", "param2"), config.params());
+ assertEquals("/data/path", config.dataPath());
+ assertEquals("none", config.authType());
+ assertEquals("json", config.inputType());
+ assertEquals("limit", config.limitQueryParam());
+ assertTrue(config.errorOn400());
+ assertNull(config.jsonOptions());
+ assertNull(config.xmlOptions());
+ assertNull(config.csvOptions());
+ assertNull(config.paginator());
+ assertTrue(config.verifySSLCert());
+ assertTrue(config.requireTail());
+ assertTrue(config.caseSensitiveFilters());
+ }
+}
diff --git a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpCSVOptions.java b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpCSVOptions.java
new file mode 100644
index 0000000..383bb0d
--- /dev/null
+++ b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpCSVOptions.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.drill.exec.store.http;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import org.apache.drill.common.util.DrillFileUtils;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class TestHttpCSVOptions {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static String CSV_OPTIONS_JSON;
+
+ @BeforeAll
+ public static void setup() throws Exception {
+ CSV_OPTIONS_JSON = Files.asCharSource(
+ DrillFileUtils.getResourceAsFile("/data/csvOptions.json"), Charsets.UTF_8
+ ).read().trim();
+ }
+
+
+ @Test
+ void testBuilderDefaults() {
+ HttpCSVOptions options = HttpCSVOptions.builder().build();
+
+ assertEquals(",", options.getDelimiter());
+ assertEquals('"', options.getQuote());
+ assertEquals('"', options.getQuoteEscape());
+ assertEquals("\n", options.getLineSeparator());
+ assertNull(options.getHeaderExtractionEnabled());
+ assertEquals(0, options.getNumberOfRowsToSkip());
+ assertEquals(-1, options.getNumberOfRecordsToRead());
+ assertTrue(options.isLineSeparatorDetectionEnabled());
+ assertEquals(512, options.getMaxColumns());
+ assertEquals(4096, options.getMaxCharsPerColumn());
+ assertTrue(options.isSkipEmptyLines());
+ assertTrue(options.isIgnoreLeadingWhitespaces());
+ assertTrue(options.isIgnoreTrailingWhitespaces());
+ assertNull(options.getNullValue());
+ }
+
+ @Test
+ void testBuilderOverride() {
+ HttpCSVOptions options = HttpCSVOptions.builder()
+ .delimiter(";")
+ .quote('\'')
+ .quoteEscape('\\')
+ .lineSeparator("\r\n")
+ .headerExtractionEnabled(false)
+ .numberOfRowsToSkip(5)
+ .numberOfRecordsToRead(10)
+ .lineSeparatorDetectionEnabled(false)
+ .maxColumns(1024)
+ .maxCharsPerColumn(8192)
+ .skipEmptyLines(false)
+ .ignoreLeadingWhitespaces(false)
+ .ignoreTrailingWhitespaces(false)
+ .nullValue("NULL")
+ .build();
+
+ assertEquals(";", options.getDelimiter());
+ assertEquals('\'', options.getQuote());
+ assertEquals('\\', options.getQuoteEscape());
+ assertEquals("\r\n", options.getLineSeparator());
+ assertFalse(options.getHeaderExtractionEnabled());
+ assertEquals(5, options.getNumberOfRowsToSkip());
+ assertEquals(10, options.getNumberOfRecordsToRead());
+ assertFalse(options.isLineSeparatorDetectionEnabled());
+ assertEquals(1024, options.getMaxColumns());
+ assertEquals(8192, options.getMaxCharsPerColumn());
+ assertFalse(options.isSkipEmptyLines());
+ assertFalse(options.isIgnoreLeadingWhitespaces());
+ assertFalse(options.isIgnoreTrailingWhitespaces());
+ assertEquals("NULL", options.getNullValue());
+ }
+
+ @Test
+ void testJSONSerialization() throws Exception {
+ HttpCSVOptions options = HttpCSVOptions.builder()
+ .delimiter(";")
+ .quote('\'')
+ .quoteEscape('\\')
+ .lineSeparator("\r\n")
+ .headerExtractionEnabled(false)
+ .numberOfRowsToSkip(5)
+ .numberOfRecordsToRead(10)
+ .lineSeparatorDetectionEnabled(false)
+ .maxColumns(1024)
+ .maxCharsPerColumn(8192)
+ .skipEmptyLines(false)
+ .ignoreLeadingWhitespaces(false)
+ .ignoreTrailingWhitespaces(false)
+ .nullValue("NULL")
+ .build();
+
+ String json = objectMapper.writeValueAsString(options);
+
+ assertNotNull(json);
+ assertEquals(CSV_OPTIONS_JSON, json);
+ }
+
+ @Test
+ public void testJSONDeserialization() throws JsonProcessingException {
+ HttpCSVOptions options = objectMapper.readValue(CSV_OPTIONS_JSON, HttpCSVOptions.class);
+
+ assertEquals(";", options.getDelimiter());
+ assertEquals('\'', options.getQuote());
+ assertEquals('\\', options.getQuoteEscape());
+ assertEquals("\r\n", options.getLineSeparator());
+ assertNull(options.getHeaderExtractionEnabled());
+ assertEquals(5, options.getNumberOfRowsToSkip());
+ assertEquals(10, options.getNumberOfRecordsToRead());
+ assertTrue(options.isLineSeparatorDetectionEnabled());
+ assertEquals(1024, options.getMaxColumns());
+ assertEquals(8192, options.getMaxCharsPerColumn());
+ assertTrue(options.isSkipEmptyLines());
+ assertTrue(options.isIgnoreLeadingWhitespaces());
+ assertTrue(options.isIgnoreTrailingWhitespaces());
+ assertEquals("NULL", options.getNullValue());
+
+ }
+
+}
diff --git a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
index 4ef5e45..9c66855 100644
--- a/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
+++ b/contrib/storage-http/src/test/java/org/apache/drill/exec/store/http/TestHttpPlugin.java
@@ -79,6 +79,8 @@
private static String TEST_XML_RESPONSE;
private static String TEST_JSON_RESPONSE_WITH_DATATYPES;
+ private static String TEST_TSV_RESPONSE;
+
public static String makeUrl(String url) {
return String.format(url, MOCK_SERVER_PORT);
}
@@ -92,6 +94,7 @@
TEST_CSV_RESPONSE = Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/response.csv"), Charsets.UTF_8).read();
TEST_XML_RESPONSE = Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/response.xml"), Charsets.UTF_8).read();
TEST_JSON_RESPONSE_WITH_DATATYPES = Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/response2.json"), Charsets.UTF_8).read();
+ TEST_TSV_RESPONSE = Files.asCharSource(DrillFileUtils.getResourceAsFile("/data/response.tsv"), Charsets.UTF_8).read();
dirTestWatcher.copyResourceToRoot(Paths.get("data/"));
makeEnhancedLiveConfig();
@@ -611,6 +614,22 @@
.inputType("csv")
.build();
+ HttpCSVOptions tsvOptions = HttpCSVOptions.builder()
+ .delimiter("\t")
+ .quote('"')
+ .build();
+ HttpApiConfig mockTsvConfig = HttpApiConfig.builder()
+ .url(makeUrl("http://localhost:%d/csv"))
+ .method("GET")
+ .headers(headers)
+ .authType("basic")
+ .userName("user")
+ .password("pass")
+ .dataPath("results")
+ .inputType("csv")
+ .csvOptions(tsvOptions)
+ .build();
+
HttpApiConfig mockCsvConfigWithPaginator = HttpApiConfig.builder()
.url(makeUrl("http://localhost:%d/csv"))
.method("get")
@@ -715,6 +734,7 @@
configs.put("mockJsonNullBodyPost", mockJsonNullBodyPost);
configs.put("mockPostPushdown", mockPostPushdown);
configs.put("mockPostPushdownWithStaticParams", mockPostPushdownWithStaticParams);
+ configs.put("mocktsv", mockTsvConfig);
configs.put("mockcsv", mockCsvConfig);
configs.put("mockxml", mockXmlConfig);
configs.put("mockxml_with_schema", mockXmlConfigWithSchema);
@@ -756,6 +776,7 @@
.addRow("local", "http")
.addRow("local.mockcsv", "http")
.addRow("local.mockpost", "http")
+ .addRow("local.mocktsv", "http")
.addRow("local.mockxml", "http")
.addRow("local.mockxml_with_schema", "http")
.addRow("local.nullpost", "http")
@@ -1677,6 +1698,36 @@
}
@Test
+ public void testTsvResponse() throws Exception {
+ String sql = "SELECT * FROM local.mocktsv.`tsv?arg1=4`";
+ try (MockWebServer server = startServer()) {
+
+ server.enqueue(new MockResponse().setResponseCode(200).setBody(TEST_TSV_RESPONSE));
+
+ RowSet results = client.queryBuilder().sql(sql).rowSet();
+
+ TupleMetadata expectedSchema = new SchemaBuilder()
+ .add("col1", TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL)
+ .add("col2", TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL)
+ .add("col3", TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL)
+ .build();
+
+ RowSet expected = new RowSetBuilder(client.allocator(), expectedSchema)
+ .addRow("1", "2", "3")
+ .addRow("4", "5", "6")
+ .build();
+
+ RowSetUtilities.verify(expected, results);
+
+ // Verify correct username/password from endpoint configuration
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertNotNull(recordedRequest.getHeader("Authorization"));
+ assertEquals("Basic dXNlcjpwYXNz", recordedRequest.getHeader("Authorization"));
+ }
+ }
+
+
+ @Test
public void testCsvResponseWithEnhancedMode() throws Exception {
String sql = "SELECT * FROM local2.mockcsv.`csv?arg1=4`";
try (MockWebServer server = startServer()) {
diff --git a/contrib/storage-http/src/test/resources/data/csvOptions.json b/contrib/storage-http/src/test/resources/data/csvOptions.json
new file mode 100644
index 0000000..6fe17c1
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/csvOptions.json
@@ -0,0 +1 @@
+{"delimiter":";","quote":"'","quoteEscape":"\\","lineSeparator":"\r\n","numberOfRowsToSkip":5,"numberOfRecordsToRead":10,"maxColumns":1024,"maxCharsPerColumn":8192,"nullValue":"NULL"}
diff --git a/contrib/storage-http/src/test/resources/data/exampleHttpApiConfig.json b/contrib/storage-http/src/test/resources/data/exampleHttpApiConfig.json
new file mode 100644
index 0000000..a72c9cc
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/exampleHttpApiConfig.json
@@ -0,0 +1 @@
+{"url":"http://example.com","requireTail":true,"method":"GET","postBody":"testBody","headers":{"Authorization":"Bearer token"},"params":["param1","param2"],"dataPath":"/data/path","authType":"none","inputType":"json","xmlDataLevel":1,"limitQueryParam":"limit","postParameterLocation":"POST_BODY","errorOn400":true,"caseSensitiveFilters":true,"verifySSLCert":true}
diff --git a/contrib/storage-http/src/test/resources/data/response.tsv b/contrib/storage-http/src/test/resources/data/response.tsv
new file mode 100644
index 0000000..bf29da1
--- /dev/null
+++ b/contrib/storage-http/src/test/resources/data/response.tsv
@@ -0,0 +1,3 @@
+"col1" "col2" "col3"
+"1" "2" "3"
+"4" "5" "6"