/* * Copyright © 2014-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.executor; import co.cask.cdap.api.data.format.FormatSpecification; import co.cask.cdap.api.data.schema.Schema; import co.cask.cdap.api.data.schema.UnsupportedTypeException; import co.cask.cdap.api.dataset.Dataset; import co.cask.cdap.api.dataset.DatasetManagementException; import co.cask.cdap.api.dataset.DatasetSpecification; import co.cask.cdap.api.dataset.lib.PartitionKey; import co.cask.cdap.api.dataset.lib.PartitionedFileSet; import co.cask.cdap.api.dataset.lib.PartitionedFileSetArguments; import co.cask.cdap.api.dataset.lib.Partitioning; import co.cask.cdap.common.BadRequestException; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.data.dataset.SystemDatasetInstantiator; import co.cask.cdap.data.dataset.SystemDatasetInstantiatorFactory; import co.cask.cdap.data2.dataset2.DatasetFramework; import co.cask.cdap.data2.transaction.stream.StreamAdmin; import co.cask.cdap.explore.service.ExploreException; import co.cask.cdap.explore.service.ExploreTableManager; import co.cask.cdap.internal.io.SchemaTypeAdapter; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.QueryHandle; import co.cask.http.AbstractHttpHandler; import co.cask.http.HttpResponder; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.google.inject.Inject; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.sql.SQLException; import java.util.Map; import javax.annotation.Nullable; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; /** * Handler that implements internal explore APIs. */ @Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}/data/explore") public class ExploreExecutorHttpHandler extends AbstractHttpHandler { private static final Logger LOG = LoggerFactory.getLogger(ExploreExecutorHttpHandler.class); private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Schema.class, new SchemaTypeAdapter()) .create(); private final ExploreTableManager exploreTableManager; private final DatasetFramework datasetFramework; private final StreamAdmin streamAdmin; private final SystemDatasetInstantiatorFactory datasetInstantiatorFactory; @Inject public ExploreExecutorHttpHandler(ExploreTableManager exploreTableManager, DatasetFramework datasetFramework, StreamAdmin streamAdmin, SystemDatasetInstantiatorFactory datasetInstantiatorFactory) { this.exploreTableManager = exploreTableManager; this.datasetFramework = datasetFramework; this.streamAdmin = streamAdmin; this.datasetInstantiatorFactory = datasetInstantiatorFactory; } @POST @Path("streams/{stream}/tables/{table}/enable") public void enableStream(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("stream") String streamName, @PathParam("table") String tableName) throws Exception { Id.Stream streamId = Id.Stream.from(namespaceId, streamName); try (Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent()))) { FormatSpecification format = GSON.fromJson(reader, FormatSpecification.class); if (format == null) { throw new BadRequestException("Expected format in the body"); } QueryHandle handle = exploreTableManager.enableStream(tableName, streamId, format); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (UnsupportedTypeException e) { LOG.error("Exception while generating create statement for stream {}", streamName, e); responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } } @POST @Path("streams/{stream}/tables/{table}/disable") public void disableStream(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("stream") String streamName, @PathParam("table") String tableName) { Id.Stream streamId = Id.Stream.from(namespaceId, streamName); try { // throws io exception if there is no stream streamAdmin.getConfig(streamId); } catch (IOException e) { LOG.debug("Could not find stream {} to disable explore on.", streamName, e); responder.sendString(HttpResponseStatus.NOT_FOUND, "Could not find stream " + streamName); return; } try { QueryHandle handle = exploreTableManager.disableStream(tableName, streamId); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (Throwable t) { LOG.error("Got exception disabling exploration for stream {}", streamId, t); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, t.getMessage()); } } /** * Enable ad-hoc exploration of a dataset instance. */ @POST @Path("datasets/{dataset}/enable") public void enableDataset(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("dataset") String datasetName) { Id.DatasetInstance datasetID = Id.DatasetInstance.from(namespaceId, datasetName); DatasetSpecification datasetSpec; try { datasetSpec = datasetFramework.getDatasetSpec(datasetID); if (datasetSpec == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Cannot load dataset " + datasetID); return; } } catch (DatasetManagementException e) { responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error getting spec for dataset " + datasetID); return; } try { QueryHandle handle = exploreTableManager.enableDataset(datasetID, datasetSpec); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (IllegalArgumentException e) { responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage()); } catch (ExploreException e) { responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error enabling explore on dataset " + datasetID); } catch (SQLException e) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "SQL exception while trying to enable explore on dataset " + datasetID); } catch (UnsupportedTypeException e) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "Schema for dataset " + datasetID + " is not supported for exploration: " + e.getMessage()); } catch (Throwable e) { LOG.error("Got exception:", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } /** * Disable ad-hoc exploration of a dataset instance. */ @POST @Path("datasets/{dataset}/disable") public void disableDataset(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("dataset") String datasetName) { LOG.debug("Disabling explore for dataset instance {}", datasetName); Id.DatasetInstance datasetID = Id.DatasetInstance.from(namespaceId, datasetName); DatasetSpecification spec; try { spec = datasetFramework.getDatasetSpec(datasetID); if (spec == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Cannot load dataset " + datasetID); return; } } catch (DatasetManagementException e) { responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error getting spec for dataset " + datasetID); return; } try { QueryHandle handle = exploreTableManager.disableDataset(datasetID, spec); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (Throwable e) { LOG.error("Got exception while trying to disable explore on dataset {}", datasetID, e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } @POST @Path("datasets/{dataset}/partitions") public void addPartition(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("dataset") String datasetName) { Id.DatasetInstance datasetInstanceId = Id.DatasetInstance.from(namespaceId, datasetName); Dataset dataset; try (SystemDatasetInstantiator datasetInstantiator = datasetInstantiatorFactory.create()) { dataset = datasetInstantiator.getDataset(datasetInstanceId); if (dataset == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Cannot load dataset " + datasetInstanceId); return; } } catch (IOException e) { String classNotFoundMessage = isClassNotFoundException(e); if (classNotFoundMessage != null) { JsonObject json = new JsonObject(); json.addProperty("handle", QueryHandle.NO_OP.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); return; } LOG.error("Exception instantiating dataset {}.", datasetInstanceId, e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Exception instantiating dataset " + datasetName); return; } try { if (!(dataset instanceof PartitionedFileSet)) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "not a partitioned dataset."); return; } Partitioning partitioning = ((PartitionedFileSet) dataset).getPartitioning(); Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent())); Map<String, String> properties = GSON.fromJson(reader, new TypeToken<Map<String, String>>() { }.getType()); String fsPath = properties.get("path"); if (fsPath == null) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "path was not specified."); return; } PartitionKey partitionKey; try { partitionKey = PartitionedFileSetArguments.getOutputPartitionKey(properties, partitioning); } catch (Exception e) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "invalid partition key: " + e.getMessage()); return; } if (partitionKey == null) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "no partition key was given."); return; } QueryHandle handle = exploreTableManager.addPartition(datasetInstanceId, partitionKey, fsPath); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (Throwable e) { LOG.error("Got exception:", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } // this should really be a DELETE request. However, the partition key must be passed in the body // of the request, and that does not work with many HTTP clients, including Java's URLConnection. @POST @Path("datasets/{dataset}/deletePartition") public void dropPartition(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId, @PathParam("dataset") String datasetName) { Id.DatasetInstance datasetInstanceId = Id.DatasetInstance.from(namespaceId, datasetName); Dataset dataset; try (SystemDatasetInstantiator datasetInstantiator = datasetInstantiatorFactory.create()) { dataset = datasetInstantiator.getDataset(datasetInstanceId); if (dataset == null) { responder.sendString(HttpResponseStatus.NOT_FOUND, "Cannot load dataset " + datasetInstanceId); return; } } catch (IOException e) { String classNotFoundMessage = isClassNotFoundException(e); if (classNotFoundMessage != null) { JsonObject json = new JsonObject(); json.addProperty("handle", QueryHandle.NO_OP.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); return; } LOG.error("Exception instantiating dataset {}.", datasetInstanceId, e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Exception instantiating dataset " + datasetInstanceId); return; } try { if (!(dataset instanceof PartitionedFileSet)) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "not a partitioned dataset."); return; } Partitioning partitioning = ((PartitionedFileSet) dataset).getPartitioning(); Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent())); Map<String, String> properties = GSON.fromJson(reader, new TypeToken<Map<String, String>>() { }.getType()); PartitionKey partitionKey; try { partitionKey = PartitionedFileSetArguments.getOutputPartitionKey(properties, partitioning); } catch (Exception e) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "invalid partition key: " + e.getMessage()); return; } if (partitionKey == null) { responder.sendString(HttpResponseStatus.BAD_REQUEST, "no partition key was given."); return; } QueryHandle handle = exploreTableManager.dropPartition(datasetInstanceId, partitionKey); JsonObject json = new JsonObject(); json.addProperty("handle", handle.getHandle()); responder.sendJson(HttpResponseStatus.OK, json); } catch (Throwable e) { LOG.error("Got exception:", e); responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } } // returns the cause of the class not found exception if it is one. Otherwise returns null. @Nullable private static String isClassNotFoundException(Throwable e) { if (e instanceof ClassNotFoundException) { return e.getMessage(); } if (e.getCause() != null) { return isClassNotFoundException(e.getCause()); } return null; } }