/*
* Copyright © 2015 Cask Data, Inc.
*
* Licensed 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 co.cask.cdap.explore.client;
import co.cask.cdap.api.data.format.FormatSpecification;
import co.cask.cdap.api.data.schema.Schema;
import co.cask.cdap.api.dataset.lib.PartitionKey;
import co.cask.cdap.api.dataset.lib.PartitionedFileSetArguments;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.explore.service.Explore;
import co.cask.cdap.explore.service.ExploreException;
import co.cask.cdap.explore.service.HandleNotFoundException;
import co.cask.cdap.explore.service.MetaDataInfo;
import co.cask.cdap.explore.service.TableNotFoundException;
import co.cask.cdap.explore.utils.ColumnsArgs;
import co.cask.cdap.explore.utils.FunctionsArgs;
import co.cask.cdap.explore.utils.SchemasArgs;
import co.cask.cdap.explore.utils.TablesArgs;
import co.cask.cdap.internal.io.SchemaTypeAdapter;
import co.cask.cdap.proto.ColumnDesc;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.QueryHandle;
import co.cask.cdap.proto.QueryInfo;
import co.cask.cdap.proto.QueryResult;
import co.cask.cdap.proto.QueryStatus;
import co.cask.cdap.proto.TableInfo;
import co.cask.cdap.proto.TableNameInfo;
import co.cask.common.http.HttpMethod;
import co.cask.common.http.HttpRequest;
import co.cask.common.http.HttpRequestConfig;
import co.cask.common.http.HttpRequests;
import co.cask.common.http.HttpResponse;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* The methods of this class call the HTTP APIs exposed by explore and return the raw information
* contained in their json responses. This class is only meant to be extended by classes
* which implement ExploreClient.
*/
abstract class ExploreHttpClient implements Explore {
private static final Logger LOG = LoggerFactory.getLogger(ExploreHttpClient.class);
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
.create();
private static final Type MAP_TYPE_TOKEN = new TypeToken<Map<String, String>>() { }.getType();
private static final Type TABLES_TYPE = new TypeToken<List<TableNameInfo>>() { }.getType();
private static final Type COL_DESC_LIST_TYPE = new TypeToken<List<ColumnDesc>>() { }.getType();
private static final Type QUERY_INFO_LIST_TYPE = new TypeToken<List<QueryInfo>>() { }.getType();
private static final Type ROW_LIST_TYPE = new TypeToken<List<QueryResult>>() { }.getType();
protected HttpRequestConfig getHttpRequestConfig() {
return HttpRequestConfig.DEFAULT;
}
protected abstract InetSocketAddress getExploreServiceAddress();
protected abstract String getAuthToken();
protected abstract boolean isSSLEnabled();
protected abstract boolean verifySSLCert();
protected boolean isAvailable() {
try {
HttpResponse response = doGet("explore/status");
return response.getResponseCode() == HttpURLConnection.HTTP_OK;
} catch (Exception e) {
LOG.info("Caught exception when checking Explore availability", e);
return false;
}
}
protected QueryHandle doEnableExploreStream(Id.Stream stream, String tableName,
FormatSpecification format) throws ExploreException {
HttpResponse response = doPost(String.format(
"namespaces/%s/data/explore/streams/%s/tables/%s/enable",
stream.getNamespaceId(), stream.getId(), tableName), format == null ? null : GSON.toJson(format), null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot enable explore on stream %s with table %s. Reason: %s",
stream.getId(), tableName, response));
}
protected QueryHandle doDisableExploreStream(Id.Stream stream, String tableName) throws ExploreException {
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/streams/%s/tables/%s/disable",
stream.getNamespaceId(), stream.getId(), tableName), null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot disable explore on stream %s with table %s. Reason: %s",
stream.getId(), tableName, response));
}
protected QueryHandle doAddPartition(Id.DatasetInstance datasetInstance,
PartitionKey key, String path) throws ExploreException {
Map<String, String> args = Maps.newHashMap();
PartitionedFileSetArguments.setOutputPartitionKey(args, key);
args.put("path", path);
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/datasets/%s/partitions",
datasetInstance.getNamespaceId(), datasetInstance.getId()),
GSON.toJson(args), null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot add partition with key %s to dataset %s. Reason: %s",
key, datasetInstance.toString(), response));
}
protected QueryHandle doDropPartition(Id.DatasetInstance datasetInstance, PartitionKey key) throws ExploreException {
Map<String, String> args = Maps.newHashMap();
PartitionedFileSetArguments.setOutputPartitionKey(args, key);
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/datasets/%s/deletePartition",
datasetInstance.getNamespaceId(), datasetInstance.getId()),
GSON.toJson(args), null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot drop partition with key %s from dataset %s. Reason: %s",
key, datasetInstance.toString(), response));
}
protected QueryHandle doEnableExploreDataset(Id.DatasetInstance datasetInstance) throws ExploreException {
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/datasets/%s/enable",
datasetInstance.getNamespaceId(),
datasetInstance.getId()), null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot enable explore on dataset %s. Reason: %s",
datasetInstance.toString(), response));
}
protected QueryHandle doDisableExploreDataset(Id.DatasetInstance datasetInstance) throws ExploreException {
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/datasets/%s/disable",
datasetInstance.getNamespaceId(), datasetInstance.getId()),
null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException(String.format("Cannot disable explore on dataset %s. Reason: %s",
datasetInstance.toString(), response));
}
@Override
public QueryHandle execute(Id.Namespace namespace, String statement) throws ExploreException {
HttpResponse response = doPost(String.format("namespaces/%s/data/explore/queries", namespace.getId()),
GSON.toJson(ImmutableMap.of("query", statement)), null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot execute query. Reason: " + response);
}
@Override
public QueryStatus getStatus(QueryHandle handle) throws ExploreException, HandleNotFoundException {
HttpResponse response = doGet(String.format("data/explore/queries/%s/%s",
handle.getHandle(), "status"));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, QueryStatus.class);
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new HandleNotFoundException("Handle " + handle.getHandle() + "not found.");
}
throw new ExploreException("Cannot get status. Reason: " + response);
}
@Override
public List<ColumnDesc> getResultSchema(QueryHandle handle) throws ExploreException, HandleNotFoundException {
HttpResponse response = doGet(String.format("data/explore/queries/%s/%s",
handle.getHandle(), "schema"));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, COL_DESC_LIST_TYPE);
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new HandleNotFoundException("Handle " + handle.getHandle() + "not found.");
}
throw new ExploreException("Cannot get result schema. Reason: " + response);
}
@Override
public List<QueryResult> nextResults(QueryHandle handle, int size) throws ExploreException, HandleNotFoundException {
HttpResponse response = doPost(String.format("data/explore/queries/%s/%s",
handle.getHandle(), "next"),
GSON.toJson(ImmutableMap.of("size", size)), null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, ROW_LIST_TYPE);
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new HandleNotFoundException("Handle " + handle.getHandle() + "not found.");
}
throw new ExploreException("Cannot get next results. Reason: " + response);
}
@Override
public List<QueryResult> previewResults(QueryHandle handle)
throws ExploreException, HandleNotFoundException, SQLException {
HttpResponse response = doPost(String.format("data/explore/queries/%s/%s",
handle.getHandle(), "preview"),
null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, ROW_LIST_TYPE);
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new HandleNotFoundException("Handle " + handle.getHandle() + "not found.");
}
throw new ExploreException("Cannot get results preview. Reason: " + response);
}
@Override
public void close(QueryHandle handle) throws ExploreException, HandleNotFoundException {
HttpResponse response = doDelete(String.format("data/explore/queries/%s", handle.getHandle()));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return;
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new HandleNotFoundException("Handle " + handle.getHandle() + "not found.");
}
throw new ExploreException("Cannot close operation. Reason: " + response);
}
@Override
public int getActiveQueryCount(Id.Namespace namespace) throws ExploreException {
String resource = String.format("namespaces/%s/data/explore/queries/count", namespace.getId());
HttpResponse response = doGet(resource);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
Map<String, String> mapResponse = parseJson(response, new TypeToken<Map<String, String>>() { }.getType());
return Integer.parseInt(mapResponse.get("count"));
}
throw new ExploreException("Cannot get list of queries. Reason: " + response);
}
@Override
public List<QueryInfo> getQueries(Id.Namespace namespace) throws ExploreException, SQLException {
String resource = String.format("namespaces/%s/data/explore/queries/", namespace.getId());
HttpResponse response = doGet(resource);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, QUERY_INFO_LIST_TYPE);
}
throw new ExploreException("Cannot get list of queries. Reason: " + response);
}
@Override
public QueryHandle getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern)
throws ExploreException, SQLException {
String body = GSON.toJson(new ColumnsArgs(catalog, schemaPattern,
tableNamePattern, columnNamePattern));
String resource = String.format("namespaces/%s/data/explore/jdbc/columns", schemaPattern);
HttpResponse response = doPost(resource, body, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the columns. Reason: " + response);
}
@Override
public QueryHandle getCatalogs() throws ExploreException, SQLException {
HttpResponse response = doPost("data/explore/jdbc/catalogs", null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the catalogs. Reason: " + response);
}
@Override
public QueryHandle getSchemas(String catalog, String schemaPattern) throws ExploreException, SQLException {
String body = GSON.toJson(new SchemasArgs(catalog, schemaPattern));
String resource = String.format("namespaces/%s/data/explore/jdbc/schemas", schemaPattern);
HttpResponse response = doPost(resource, body, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the schemas. Reason: " + response);
}
@Override
public QueryHandle getFunctions(String catalog, String schemaPattern, String functionNamePattern)
throws ExploreException, SQLException {
String body = GSON.toJson(new FunctionsArgs(catalog, schemaPattern, functionNamePattern));
String resource = String.format("namespaces/%s/data/explore/jdbc/functions", schemaPattern);
HttpResponse response = doPost(resource, body, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the functions. Reason: " + response);
}
@Override
public MetaDataInfo getInfo(MetaDataInfo.InfoType infoType) throws ExploreException, SQLException {
HttpResponse response = doGet(String.format("data/explore/jdbc/info/%s", infoType.name()));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, MetaDataInfo.class);
}
throw new ExploreException("Cannot get information " + infoType.name() + ". Reason: " + response);
}
@Override
public QueryHandle getTables(String catalog, String schemaPattern,
String tableNamePattern, List<String> tableTypes) throws ExploreException, SQLException {
String body = GSON.toJson(new TablesArgs(catalog, schemaPattern, tableNamePattern, tableTypes));
String resource = String.format("namespaces/%s/data/explore/jdbc/tables", schemaPattern);
HttpResponse response = doPost(resource, body, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the tables. Reason: " + response);
}
@Override
public List<TableNameInfo> getTables(@Nullable String database) throws ExploreException {
HttpResponse response = doGet(String.format("namespaces/%s/data/explore/tables", database));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, TABLES_TYPE);
}
throw new ExploreException("Cannot get the tables. Reason: " + response);
}
@Override
public TableInfo getTableInfo(@Nullable String database, String table)
throws ExploreException, TableNotFoundException {
HttpResponse response = doGet(String.format("namespaces/%s/data/explore/tables/%s/info", database, table));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return parseJson(response, TableInfo.class);
} else if (response.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
throw new TableNotFoundException("Table " + database + table + " not found.");
}
throw new ExploreException("Cannot get the schema of table " + database + table +
". Reason: " + response);
}
@Override
public QueryHandle getTableTypes() throws ExploreException, SQLException {
HttpResponse response = doPost("data/explore/jdbc/tableTypes", null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the tables. Reason: " + response);
}
@Override
public QueryHandle getTypeInfo() throws ExploreException, SQLException {
HttpResponse response = doPost("data/explore/jdbc/types", null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot get the tables. Reason: " + response);
}
@Override
public QueryHandle createNamespace(Id.Namespace namespace) throws ExploreException, SQLException {
HttpResponse response = doPut(String.format("data/explore/namespaces/%s", namespace.getId()), null, null);
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot add a namespace. Reason: " + response);
}
@Override
public QueryHandle deleteNamespace(Id.Namespace namespace) throws ExploreException, SQLException {
HttpResponse response = doDelete(String.format("data/explore/namespaces/%s", namespace.getId()));
if (response.getResponseCode() == HttpURLConnection.HTTP_OK) {
return QueryHandle.fromId(parseResponseAsMap(response, "handle"));
}
throw new ExploreException("Cannot remove a namespace. Reason: " + response);
}
private String parseResponseAsMap(HttpResponse response, String key) throws ExploreException {
Map<String, String> responseMap = parseJson(response, MAP_TYPE_TOKEN);
if (responseMap.containsKey(key)) {
return responseMap.get(key);
}
String message = String.format("Cannot find key %s in server response: %s", key,
new String(response.getResponseBody(), Charsets.UTF_8));
LOG.error(message);
throw new ExploreException(message);
}
private <T> T parseJson(HttpResponse response, Type type) throws ExploreException {
String responseString = response.getResponseBodyAsString();
try {
return GSON.fromJson(responseString, type);
} catch (JsonParseException e) {
String message = String.format("Cannot parse server response: %s", responseString);
LOG.error(message, e);
throw new ExploreException(message, e);
}
}
private HttpResponse doGet(String resource) throws ExploreException {
return doRequest(resource, "GET", null, null);
}
private HttpResponse doPost(String resource, String body, Map<String, String> headers) throws ExploreException {
return doRequest(resource, "POST", headers, body);
}
private HttpResponse doPut(String resource, String body, Map<String, String> headers) throws ExploreException {
return doRequest(resource, "PUT", headers, body);
}
private HttpResponse doDelete(String resource) throws ExploreException {
return doRequest(resource, "DELETE", null, null);
}
private HttpResponse doRequest(String resource, String requestMethod,
@Nullable Map<String, String> headers,
@Nullable String body) throws ExploreException {
Map<String, String> newHeaders = headers;
if (getAuthToken() != null && !getAuthToken().isEmpty()) {
newHeaders = (headers != null) ? Maps.newHashMap(headers) : Maps.<String, String>newHashMap();
newHeaders.put("Authorization", "Bearer " + getAuthToken());
}
String resolvedUrl = resolve(resource);
try {
URL url = new URL(resolvedUrl);
HttpRequest.Builder builder = HttpRequest.builder(HttpMethod.valueOf(requestMethod), url).addHeaders(newHeaders);
if (body != null) {
builder.withBody(body);
}
return HttpRequests.execute(builder.build(), createRequestConfig());
} catch (IOException e) {
throw new ExploreException(
String.format("Error connecting to Explore Service at %s while doing %s with headers %s and body %s",
resolvedUrl, requestMethod,
newHeaders == null ? "null" : Joiner.on(",").withKeyValueSeparator("=").join(newHeaders),
body == null ? "null" : body), e);
}
}
private HttpRequestConfig createRequestConfig() {
return new HttpRequestConfig(getHttpRequestConfig().getConnectTimeout(),
getHttpRequestConfig().getReadTimeout(),
verifySSLCert());
}
private String resolve(String resource) {
InetSocketAddress addr = getExploreServiceAddress();
String url = String.format("%s://%s:%s%s/%s", isSSLEnabled() ? "https" : "http",
addr.getHostName(), addr.getPort(),
Constants.Gateway.API_VERSION_3, resource);
LOG.trace("Explore URL = {}", url);
return url;
}
}