Improve POST handling, start adding tests, add LICENSE file
diff --git a/LICENSE b/LICENSE
index f927b76..861f99b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -203,8 +203,3 @@
See the License for the specific language governing permissions and
limitations under the License.
-------------------------------------------------------------
-
-
-
-APPENDIX B: Additional licenses relevant to this product:
- (none)
diff --git a/NOTICE b/NOTICE
index 01aca2a..8ff8dcf 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
-Apache DataSketches Server
-Copyright 2020 - The Apache Software Foundation
+Apache DataSketches-server
+Copyright 2021 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
diff --git a/src/main/java/org/apache/datasketches/server/SketchConstants.java b/src/main/java/org/apache/datasketches/server/SketchConstants.java
index 742dfc9..1e4059b 100644
--- a/src/main/java/org/apache/datasketches/server/SketchConstants.java
+++ b/src/main/java/org/apache/datasketches/server/SketchConstants.java
@@ -21,12 +21,12 @@
public final class SketchConstants {
// API call paths, relative to root
- public static final String UPDATE_PATH = "/update";
- public static final String SERIALIZE_PATH = "/serialize";
- public static final String STATUS_PATH = "/status";
- public static final String QUERY_PATH = "/query";
- public static final String MERGE_PATH = "/merge";
- public static final String RESET_PATH = "/reset";
+ public static final String UPDATE_PATH = "update";
+ public static final String SERIALIZE_PATH = "serialize";
+ public static final String STATUS_PATH = "status";
+ public static final String QUERY_PATH = "query";
+ public static final String MERGE_PATH = "merge";
+ public static final String RESET_PATH = "reset";
// JSON Query/Update/Merge Field Names
public static final String QUERY_NAME_FIELD = "name";
diff --git a/src/main/java/org/apache/datasketches/server/SketchServer.java b/src/main/java/org/apache/datasketches/server/SketchServer.java
index b70dca6..e0a8a0e 100644
--- a/src/main/java/org/apache/datasketches/server/SketchServer.java
+++ b/src/main/java/org/apache/datasketches/server/SketchServer.java
@@ -50,30 +50,42 @@
// defines paths and registers the relevant handlers
private void createServer() {
- server = new Server(config.getPort());
+ server = new Server();
+
+ // configure port
+ final ServerConnector http = new ServerConnector(server);
+ http.setHost("localhost");
+ http.setPort(config.getPort());
+ server.addConnector(http);
// Error page unless you have a correct URL
final ContextHandler contextRoot = new ContextHandler("/");
- contextRoot.setContextPath("/");
contextRoot.setErrorHandler(new ErrorHandler());
- final ContextHandler contextStatus = new ContextHandler(STATUS_PATH);
+ // Add specific handlers
+ final ContextHandler contextStatus = new ContextHandler("/" + STATUS_PATH);
contextStatus.setHandler(new StatusHandler(sketches));
+ contextStatus.setAllowNullPathInfo(true);
- final ContextHandler contextSerialize = new ContextHandler(SERIALIZE_PATH);
+ final ContextHandler contextSerialize = new ContextHandler("/" + SERIALIZE_PATH);
contextSerialize.setHandler(new SerializationHandler(sketches));
+ contextSerialize.setAllowNullPathInfo(true);
- final ContextHandler contextUpdate = new ContextHandler(UPDATE_PATH);
+ final ContextHandler contextUpdate = new ContextHandler("/" + UPDATE_PATH);
contextUpdate.setHandler(new UpdateHandler(sketches));
+ contextUpdate.setAllowNullPathInfo(true);
- final ContextHandler contextMerge = new ContextHandler(MERGE_PATH);
+ final ContextHandler contextMerge = new ContextHandler("/" + MERGE_PATH);
contextMerge.setHandler(new MergeHandler(sketches));
+ contextMerge.setAllowNullPathInfo(true);
- final ContextHandler contextQuery = new ContextHandler(QUERY_PATH);
+ final ContextHandler contextQuery = new ContextHandler("/" + QUERY_PATH);
contextQuery.setHandler(new DataQueryHandler(sketches));
+ contextQuery.setAllowNullPathInfo(true);
- final ContextHandler contextReset = new ContextHandler(RESET_PATH);
+ final ContextHandler contextReset = new ContextHandler("/" + RESET_PATH);
contextReset.setHandler(new ResetHandler(sketches));
+ contextReset.setAllowNullPathInfo(true);
final ContextHandlerCollection contexts =
new ContextHandlerCollection(contextRoot,
@@ -117,6 +129,34 @@
return -1;
}
+ /**
+ * Returns the server's running status
+ * @return True for a running server, otherwise false
+ */
+ public boolean isRunning() {
+ return server != null && server.isRunning();
+ }
+
+ /**
+ * Stops the server from running. Cannot be be restarted without creating new sketches.
+ * @throws Exception Upon underlying server throwing an Exception
+ */
+ public void stop() throws Exception {
+ if (server != null) {
+ server.stop();
+ server.isStarted();
+ }
+ }
+
+ /**
+ * Package-private test method to get a specific SketchEntry
+ * @param name The name of the desired sketch
+ * @return The SketchEntry containing the sketch and type info
+ */
+ SketchStorage.SketchEntry getSketch(@NonNull final String name) {
+ return sketches.getSketch(name);
+ }
+
public static void main(final String[] args) throws Exception {
if (args.length < 1) {
System.err.println("Usage: SketchServer <config_file>");
diff --git a/src/test/java/org/apache/datasketches/server/ServerTestBase.java b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
new file mode 100644
index 0000000..8c0d7f5
--- /dev/null
+++ b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
@@ -0,0 +1,140 @@
+/*
+ * 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.datasketches.server;
+
+import static org.testng.Assert.fail;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+import java.io.DataOutputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+public class ServerTestBase {
+ final static String RESPONSE_FIELD = "response";
+
+ SketchServer server_ = null;
+ String serverUri_ = null;
+
+ @BeforeClass
+ public void launchServer() {
+ final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ server_ = new SketchServer(Objects.requireNonNull(classLoader.getResource("test_config.json")).getFile());
+ server_.start();
+ serverUri_ = server_.getURI();
+ } catch (final Exception e) {
+ fail();
+ }
+ }
+
+ @AfterClass
+ public void shutdownServer() {
+ if (server_ != null) {
+ try {
+ server_.stop();
+ } catch (final Exception e) {
+ fail();
+ }
+ }
+ }
+
+ int postData(@NonNull final String path,
+ @NonNull final JsonObject data,
+ @NonNull final JsonObject response) {
+ HttpURLConnection http = null;
+ int status = -1;
+
+ try {
+ // set up the POST
+ final URL url = new URL(serverUri_ + path);
+ http = (HttpURLConnection) url.openConnection();
+ http.setDoInput(true);
+ http.setDoOutput(true);
+ http.setRequestMethod("POST");
+ http.setRequestProperty("Content-Type", "application/json");
+ http.setRequestProperty("Accept", "application/json");
+
+ final byte[] jsonBytes = data.toString().getBytes(StandardCharsets.UTF_8);
+ http.setRequestProperty("Content-length", Integer.toString(jsonBytes.length));
+
+ // write JSON data to to stream
+ try (final DataOutputStream os = new DataOutputStream(http.getOutputStream())) {
+ os.write(jsonBytes);
+ }
+
+ status = http.getResponseCode();
+ if (status == HttpServletResponse.SC_OK) {
+ // read response, if any, and put into a JSON element
+ try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
+ response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
+ }
+ }
+ } catch (final IOException e) {
+ fail();
+ } finally {
+ if (http != null)
+ http.disconnect();
+ }
+
+ return status;
+ }
+
+ int getData(@NonNull final String path,
+ @NonNull final JsonObject data,
+ @NonNull final JsonObject response) {
+ HttpURLConnection http = null;
+ int status = -1;
+
+ try {
+ // set up the POST
+ final URL url = new URL(serverUri_ + path + "?" + data);
+ http = (HttpURLConnection) url.openConnection();
+ http.setDoInput(true);
+ http.setRequestProperty("Content-Type", "application/json");
+ http.connect();
+
+ status = http.getResponseCode();
+ if (status == HttpServletResponse.SC_OK) {
+ // read response, if any, and put into a JSON element
+ try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
+ response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
+ }
+ }
+ } catch (final IOException e) {
+ fail();
+ } finally {
+ if (http != null)
+ http.disconnect();
+ }
+
+ return status;
+ }
+}
diff --git a/src/test/java/org/apache/datasketches/server/SketchServerConfigTest.java b/src/test/java/org/apache/datasketches/server/SketchServerConfigTest.java
index 23dac17..861d58a 100644
--- a/src/test/java/org/apache/datasketches/server/SketchServerConfigTest.java
+++ b/src/test/java/org/apache/datasketches/server/SketchServerConfigTest.java
@@ -45,8 +45,8 @@
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
final SketchServerConfig serverConf =
- new SketchServerConfig(Objects.requireNonNull(classLoader.getResource("test_config.json")).getFile());
- assertEquals(serverConf.getSketchList().size(), 15);
+ new SketchServerConfig(Objects.requireNonNull(classLoader.getResource("config_with_port.json")).getFile());
+ assertEquals(serverConf.getSketchList().size(), 2);
assertEquals(serverConf.getPort(), 8080);
} catch (final IOException e) {
fail();
diff --git a/src/test/java/org/apache/datasketches/server/SketchServerTest.java b/src/test/java/org/apache/datasketches/server/SketchServerTest.java
index 757c855..61a6d04 100644
--- a/src/test/java/org/apache/datasketches/server/SketchServerTest.java
+++ b/src/test/java/org/apache/datasketches/server/SketchServerTest.java
@@ -20,6 +20,7 @@
package org.apache.datasketches.server;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@@ -43,17 +44,23 @@
// check that port and URI are invalid before starting the server
assertNotNull(server);
+ assertFalse(server.isRunning());
assertNull(server.getURI());
assertEquals(server.getPort(), -1);
try {
server.start();
+ assertTrue(server.isRunning());
+
+ // add the few tests in the try block for code simplicity
+ assertEquals(server.getPort(), 8080);
+ // initial testing suggests it's just using the host's IP address so just checking that the port
+ // is working correctly
+ assertTrue(server.getURI().endsWith(":" + server.getPort() + "/"));
+
+ server.stop();
+ assertFalse(server.isRunning());
} catch (final Exception e) {
fail();
}
-
- assertEquals(server.getPort(), 8080);
- // initial testing suggests it's just using the host's IP address so just checking that the port
- // is working correctly
- assertTrue(server.getURI().endsWith(":" + server.getPort() + "/"));
}
}
diff --git a/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
new file mode 100644
index 0000000..49969bb
--- /dev/null
+++ b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.datasketches.server;
+
+import static org.apache.datasketches.server.SketchConstants.QUERY_PAIR_ITEM_FIELD;
+import static org.apache.datasketches.server.SketchConstants.QUERY_PAIR_WEIGHT_FIELD;
+import static org.apache.datasketches.server.SketchConstants.UPDATE_PATH;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.datasketches.cpc.CpcSketch;
+import org.testng.annotations.Test;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+
+public class UpdateHandlerTest extends ServerTestBase {
+ /* The tests here are going to be structured very similarly. It might be possible
+ * to find a common framework and reduce the amount o repetition? But not clear with
+ * type erasure as opposed to C++-style templates.
+ */
+
+ @Test
+ public void cpcUpdate() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "cpcOfNumbers";
+ final int nPoints = 1000;
+
+ // testing using both GET and POST
+ int status;
+
+ JsonObject request = new JsonObject();
+ JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ status = postData(UPDATE_PATH, request, response);
+ assertEquals(status, HttpServletResponse.SC_OK);
+
+ request = new JsonObject();
+ data = new JsonArray();
+ for (int i = nPoints; i < 2 * nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ status = getData(UPDATE_PATH, request, response);
+ assertEquals(status, HttpServletResponse.SC_OK);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CpcSketch sk = (CpcSketch) entry.sketch;
+ assertEquals(sk.getEstimate(), 2 * nPoints, 2 * nPoints * 1e-2);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void fiUpdate() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "topItems";
+
+ // single item
+ JsonObject request = new JsonObject();
+ request.addProperty(sketchName, "item1");
+ assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+
+ // item with weight
+ request = new JsonObject();
+ JsonObject data = new JsonObject();
+ data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
+ data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+
+ // array of items with and without weights
+ request = new JsonObject();
+ final JsonArray dataArray = new JsonArray();
+ dataArray.add("item1"); // increases count to 2
+ data = new JsonObject();
+ data.addProperty(QUERY_PAIR_ITEM_FIELD, "item3");
+ data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 10);
+ dataArray.add(data);
+ request.add(sketchName, dataArray);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final org.apache.datasketches.frequencies.ItemsSketch<String> sk = (org.apache.datasketches.frequencies.ItemsSketch<String>) entry.sketch;
+ assertEquals(sk.getEstimate("item1"), 2);
+ assertEquals(sk.getEstimate("item2"), 5);
+ assertEquals(sk.getEstimate("item3"), 10);
+ }
+
+ @Test
+ public void hllUpdate() {
+ // update multiple sketches from an array
+ }
+
+ @Test
+ public void kllUpdate() {
+
+ }
+
+ @Test
+ public void thetaUpdate() {
+
+ }
+
+ @Test
+ public void reservoirUpdate() {
+
+ }
+
+ @Test
+ public void voUpdate() {
+
+ }
+
+}
diff --git a/src/test/resources/config_with_port.json b/src/test/resources/config_with_port.json
new file mode 100644
index 0000000..f148049
--- /dev/null
+++ b/src/test/resources/config_with_port.json
@@ -0,0 +1,15 @@
+{
+ "port": 8080,
+ "sketches_A": [
+ { "name": "cpcOfNumbers",
+ "k": 12,
+ "type": "long",
+ "family": "cpc"
+ },
+ { "name": "cpcOfStrings",
+ "k": 14,
+ "type": "string",
+ "family": "cpc"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json
index 172086e..a685c17 100644
--- a/src/test/resources/test_config.json
+++ b/src/test/resources/test_config.json
@@ -1,5 +1,5 @@
{
- "port": 8080,
+ "port": 0,
"sketches_A": [
{ "name": "cpcOfNumbers",
"k": 12,
@@ -10,7 +10,7 @@
"k": 14,
"type": "string",
"family": "cpc"
- }
+ }
],
"set1": {
"family": "hll",