/*
* Copyright © 2014 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.jdbc;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.proto.ColumnDesc;
import co.cask.cdap.proto.QueryHandle;
import co.cask.cdap.proto.QueryResult;
import co.cask.cdap.proto.QueryStatus;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.HttpResponder;
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
*
*/
public class ExploreDriverTest {
private static String exploreServiceUrl;
private static MockHttpService httpService;
@BeforeClass
public static void start() throws Exception {
httpService = new MockHttpService(new MockExploreExecutorHandler());
httpService.startAndWait();
Class.forName("co.cask.cdap.explore.jdbc.ExploreDriver");
exploreServiceUrl = String.format("%s%s:%d", Constants.Explore.Jdbc.URL_PREFIX, "localhost", httpService.getPort());
exploreServiceUrl += "?namespace=testNamespace";
}
@AfterClass
public static void stop() throws Exception {
httpService.stopAndWait();
}
@Test
public void testDriverConnection() throws Exception {
ExploreDriver driver = new ExploreDriver();
// Wrong URL format
Assert.assertNull(driver.connect("foobar", null));
// Correct format but wrong host
try {
driver.connect(Constants.Explore.Jdbc.URL_PREFIX + "foo:10000", null);
Assert.fail();
} catch (SQLException expected) {
// Expected, host is not available (random host)
}
// Correct host, but ssl enabled, so the connection fails
try {
Assert.assertNotNull(driver.connect(exploreServiceUrl + "&ssl.enabled=true", null));
Assert.fail();
} catch (SQLException expected) {
// Expected - no connection available via ssl
}
// Correct host
Assert.assertNotNull(driver.connect(exploreServiceUrl, null));
// Correct host
Assert.assertNotNull(driver.connect(exploreServiceUrl + "&ssl.enabled=false", null));
// Correct host and extra parameter
Assert.assertNotNull(driver.connect(exploreServiceUrl + "&auth.token=bar", null));
// Correct host and extra parameter
Assert.assertNotNull(driver.connect(exploreServiceUrl + "&auth.token", null));
}
@Test
public void testExploreDriver() throws Exception {
Connection connection = DriverManager.getConnection(exploreServiceUrl);
PreparedStatement statement;
ResultSet resultSet;
// Use statement.executeQuery
statement = connection.prepareStatement("fake sql query");
resultSet = statement.executeQuery();
Assert.assertEquals(resultSet, statement.getResultSet());
Assert.assertTrue(resultSet.next());
Assert.assertEquals(1, resultSet.getInt(1));
Assert.assertEquals("one", resultSet.getString(2));
Assert.assertTrue(resultSet.next());
Assert.assertEquals(2, resultSet.getInt(1));
Assert.assertEquals("two", resultSet.getString(2));
Assert.assertFalse(resultSet.next());
resultSet.close();
try {
resultSet.next();
Assert.fail();
} catch (SQLException e) {
// Expected exception: resultSet is closed
}
statement.close();
// Use statement.execute
statement = connection.prepareStatement("fake sql query 2");
Assert.assertTrue(statement.execute());
resultSet = statement.getResultSet();
Assert.assertTrue(resultSet.next());
Assert.assertEquals(1, resultSet.getInt("column1"));
Assert.assertEquals("one", resultSet.getString("column2"));
Assert.assertTrue(resultSet.next());
Assert.assertEquals(2, resultSet.getInt("column1"));
Assert.assertEquals("two", resultSet.getString("column2"));
Assert.assertFalse(resultSet.next());
resultSet.close();
try {
resultSet.next();
Assert.fail();
} catch (SQLException e) {
// Expected exception: resultSet is closed
}
statement.close();
}
@Test(timeout = 2000L)
public void testCancelQuery() throws Exception {
Connection connection = DriverManager.getConnection(exploreServiceUrl);
final PreparedStatement statement = connection.prepareStatement(MockExploreExecutorHandler.LONG_RUNNING_QUERY);
new Thread(new Runnable() {
@Override
public void run() {
try {
// Wait to make sure that statement.execute() is running
// NOTE: There can still be a race condition if the execute method fails to set the handle early enough
TimeUnit.MILLISECONDS.sleep(200);
statement.cancel();
} catch (Exception e) {
Throwables.propagate(e);
}
}
}).start();
Assert.assertFalse(statement.execute());
ResultSet rs = statement.getResultSet();
Assert.assertNull(rs);
}
@Path(Constants.Gateway.API_VERSION_3)
public static class MockExploreExecutorHandler extends AbstractHttpHandler {
static final String LONG_RUNNING_QUERY = "long_running_query";
private static final Set<String> handleWithFetchedResutls = Sets.newHashSet();
private static final Set<String> closedHandles = Sets.newHashSet();
private static final Set<String> canceledHandles = Sets.newHashSet();
private static final Set<String> longRunningQueries = Sets.newHashSet();
@GET
@Path("explore/status")
public void status(HttpRequest request, HttpResponder responder) {
responder.sendString(HttpResponseStatus.OK, "OK.\n");
}
@POST
@Path("namespaces/{namespace-id}/data/explore/queries")
public void query(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId) {
try {
QueryHandle handle = QueryHandle.generate();
Map<String, String> args = decodeArguments(request);
if (LONG_RUNNING_QUERY.equals(args.get("query"))) {
longRunningQueries.add(handle.getHandle());
}
responder.sendJson(HttpResponseStatus.OK, handle);
} catch (IOException e) {
responder.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
@DELETE
@Path("data/explore/queries/{id}")
public void closeQuery(HttpRequest request, HttpResponder responder, @PathParam("id") String id) {
if (closedHandles.contains(id)) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
return;
}
closedHandles.add(id);
responder.sendStatus(HttpResponseStatus.OK);
}
@GET
@Path("data/explore/queries/{id}/status")
public void getQueryStatus(HttpRequest request, HttpResponder responder, @PathParam("id") String id) {
QueryStatus status;
if (closedHandles.contains(id)) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
return;
} else if (canceledHandles.contains(id)) {
status = new QueryStatus(QueryStatus.OpStatus.CANCELED, false);
} else if (longRunningQueries.contains(id)) {
status = new QueryStatus(QueryStatus.OpStatus.RUNNING, false);
} else {
status = new QueryStatus(QueryStatus.OpStatus.FINISHED, true);
}
responder.sendJson(HttpResponseStatus.OK, status);
}
@GET
@Path("data/explore/queries/{id}/schema")
public void getQueryResultsSchema(HttpRequest request, HttpResponder responder, @PathParam("id") String id) {
if (closedHandles.contains(id)) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
return;
}
List<ColumnDesc> schema = ImmutableList.of(
new ColumnDesc("column1", "INT", 1, ""),
new ColumnDesc("column2", "STRING", 2, "")
);
responder.sendJson(HttpResponseStatus.OK, schema);
}
@POST
@Path("data/explore/queries/{id}/next")
public void getQueryNextResults(HttpRequest request, HttpResponder responder, @PathParam("id") String id) {
if (closedHandles.contains(id)) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
return;
}
List<QueryResult> rows = Lists.newArrayList();
if (!canceledHandles.contains(id) && !handleWithFetchedResutls.contains(id)) {
rows.add(new QueryResult(ImmutableList.<Object>of("1", "one")));
rows.add(new QueryResult(ImmutableList.<Object>of("2", "two")));
handleWithFetchedResutls.add(id);
}
responder.sendJson(HttpResponseStatus.OK, rows);
}
private Map<String, String> decodeArguments(HttpRequest request) throws IOException {
ChannelBuffer content = request.getContent();
if (!content.readable()) {
return ImmutableMap.of();
}
try (Reader reader = new InputStreamReader(new ChannelBufferInputStream(content), Charsets.UTF_8)) {
Map<String, String> args = new Gson().fromJson(reader, new TypeToken<Map<String, String>>() { }.getType());
return args == null ? ImmutableMap.<String, String>of() : args;
}
}
}
}