ISSUE #2388: Bookie Http Service Servlet for Embedded Bookie

Descriptions of the changes in this PR:



### Motivation

Expose bookie http service by servlet 

### Changes

*  Introduce Servlet for bookie http service
*  Jetty http server and test bookie http service servlet

Master Issue: #2388


Reviewers: Sijie Guo <sijie@apache.org>, Enrico Olivelli <eolivelli@gmail.com>

This closes #2403 from rudy2steiner/bookie_servlet, closes #2388
diff --git a/bookkeeper-http/pom.xml b/bookkeeper-http/pom.xml
index 8f44a0e..98fb047 100644
--- a/bookkeeper-http/pom.xml
+++ b/bookkeeper-http/pom.xml
@@ -29,5 +29,6 @@
   <modules>
     <module>http-server</module>
     <module>vertx-http-server</module>
+    <module>servlet-http-server</module>
   </modules>
 </project>
diff --git a/bookkeeper-http/servlet-http-server/pom.xml b/bookkeeper-http/servlet-http-server/pom.xml
new file mode 100644
index 0000000..eba74e7
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>bookkeeper</artifactId>
+        <groupId>org.apache.bookkeeper</groupId>
+        <version>4.12.0-SNAPSHOT</version>
+        <relativePath>../..</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.apache.bookkeeper.http</groupId>
+    <artifactId>servlet-http-server</artifactId>
+    <name>Apache BookKeeper :: Bookkeeper Http :: Servlet Http Server</name>
+    <url>http://maven.apache.org</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.bookkeeper.http</groupId>
+            <artifactId>http-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-webapp</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieHttpServiceServlet.java b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieHttpServiceServlet.java
new file mode 100644
index 0000000..966eceb
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieHttpServiceServlet.java
@@ -0,0 +1,138 @@
+/**
+ *
+ * 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.bookkeeper.http.servlet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.bookkeeper.http.AbstractHttpHandlerFactory;
+import org.apache.bookkeeper.http.HttpRouter;
+import org.apache.bookkeeper.http.HttpServer;
+import org.apache.bookkeeper.http.HttpServer.ApiType;
+import org.apache.bookkeeper.http.HttpServiceProvider;
+import org.apache.bookkeeper.http.service.ErrorHttpService;
+import org.apache.bookkeeper.http.service.HttpEndpointService;
+import org.apache.bookkeeper.http.service.HttpServiceRequest;
+import org.apache.bookkeeper.http.service.HttpServiceResponse;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bookie http service servlet.
+ *
+ **/
+public class BookieHttpServiceServlet extends HttpServlet {
+  static final Logger LOG = LoggerFactory.getLogger(BookieHttpServiceServlet.class);
+
+  // url to api
+  private final Map<String/*url*/, ApiType/*api*/> mappings = new ConcurrentHashMap<>();
+
+  public BookieHttpServiceServlet(){
+    HttpRouter<ApiType> router = new HttpRouter<HttpServer.ApiType>(
+      new AbstractHttpHandlerFactory<ApiType>(BookieServletHttpServer.getBookieHttpServiceProvider()) {
+        @Override
+        public HttpServer.ApiType newHandler(HttpServer.ApiType apiType) {
+          return apiType;
+        }
+      }) {
+      @Override
+      public void bindHandler(String endpoint, HttpServer.ApiType mapping) {
+        mappings.put(endpoint, mapping);
+      }
+    };
+    router.bindAll();
+  }
+
+  @Override
+  protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
+    HttpServiceRequest request = new HttpServiceRequest()
+                                .setMethod(httpServerMethod(httpRequest))
+                                .setParams(httpServletParams(httpRequest))
+                                .setBody(IOUtils.toString(httpRequest.getInputStream(), "UTF-8"));
+    String uri = httpRequest.getRequestURI();
+    HttpServiceResponse response;
+    try {
+      HttpServer.ApiType apiType = mappings.get(uri);
+      HttpServiceProvider bookie = BookieServletHttpServer.getBookieHttpServiceProvider();
+      if (bookie == null) {
+        httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        return;
+      }
+      HttpEndpointService httpEndpointService = bookie.provideHttpEndpointService(apiType);
+      if (httpEndpointService == null) {
+        httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return;
+      }
+      response = httpEndpointService.handle(request);
+    } catch (Throwable e) {
+      LOG.error("Error while service Bookie API request " + uri, e);
+      response = new ErrorHttpService().handle(request);
+    }
+    if (response != null) {
+      httpResponse.setStatus(response.getStatusCode());
+      try (Writer out = httpResponse.getWriter()) {
+        out.write(response.getBody());
+      }
+    } else {
+      httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+
+  /**
+   * Convert http request parameters to a map.
+   */
+  @SuppressWarnings("unchecked")
+  Map<String, String> httpServletParams(HttpServletRequest request) {
+    Map<String, String> map = new HashMap<>();
+    for (Enumeration<String> param = request.getParameterNames();
+         param.hasMoreElements();) {
+      String pName = param.nextElement();
+      map.put(pName, request.getParameter(pName));
+    }
+    return map;
+  }
+
+  /**
+   * Get servlet request method and convert to the method that can be recognized by HttpServer.
+   */
+  HttpServer.Method httpServerMethod(HttpServletRequest request) {
+    switch (request.getMethod()) {
+      case "POST":
+        return HttpServer.Method.POST;
+      case "DELETE":
+        return HttpServer.Method.DELETE;
+      case "PUT":
+        return HttpServer.Method.PUT;
+      case "GET":
+        return HttpServer.Method.GET;
+      default:
+        throw new UnsupportedOperationException("Unsupported http method");
+    }
+  }
+}
diff --git a/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieServletHttpServer.java b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieServletHttpServer.java
new file mode 100644
index 0000000..122defd
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/BookieServletHttpServer.java
@@ -0,0 +1,74 @@
+/**
+ *
+ * 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.bookkeeper.http.servlet;
+
+import org.apache.bookkeeper.http.HttpServer;
+import org.apache.bookkeeper.http.HttpServiceProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Only use for hold Http service provider,not a fully implement bookie http service server.
+ **/
+public class BookieServletHttpServer implements HttpServer {
+  static final Logger LOG = LoggerFactory.getLogger(BookieServletHttpServer.class);
+  private static HttpServiceProvider bookieHttpServiceProvider;
+  private static int listenPort = -1;
+
+  public static HttpServiceProvider getBookieHttpServiceProvider(){
+    return bookieHttpServiceProvider;
+  }
+  /**
+   * Listen  port.
+   **/
+  public static int getListenPort(){
+    return listenPort;
+  }
+
+  @Override
+  public void initialize(HttpServiceProvider httpServiceProvider) {
+     setHttpServiceProvider(httpServiceProvider);
+     LOG.info("Bookie HTTP Server initialized: {}", httpServiceProvider);
+  }
+
+  public static synchronized void setHttpServiceProvider(HttpServiceProvider httpServiceProvider){
+    bookieHttpServiceProvider = httpServiceProvider;
+  }
+
+  public static synchronized void setPort(int port){
+    listenPort = port;
+  }
+  @Override
+  public boolean startServer(int port) {
+    setPort(port);
+    return true;
+  }
+
+  @Override
+  public void stopServer() {
+
+  }
+
+  @Override
+  public boolean isRunning() {
+    return true;
+  }
+}
diff --git a/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/package-info.java b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/package-info.java
new file mode 100644
index 0000000..bdbb4f8
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/src/main/java/org/apache/bookkeeper/http/servlet/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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 for Servlet based http server.
+ */
+package org.apache.bookkeeper.http.servlet;
diff --git a/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/JettyHttpServer.java b/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/JettyHttpServer.java
new file mode 100644
index 0000000..94217d4
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/JettyHttpServer.java
@@ -0,0 +1,80 @@
+/**
+ *
+ * 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.bookkeeper.http.servlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.util.List;
+import javax.servlet.Servlet;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.webapp.WebAppContext;
+
+/**
+ * Jetty based http server.
+ **/
+
+public class JettyHttpServer {
+
+  private Server jettyServer;
+  private ContextHandlerCollection contexts;
+
+  public JettyHttpServer(String host, int port){
+     this.jettyServer = new Server(new InetSocketAddress(host, port));
+     this.contexts = new ContextHandlerCollection();
+     this.jettyServer.setHandler(contexts);
+  }
+  /**
+   * Add servlet.
+   **/
+  public void addServlet(String webApp, String contextPath, String pathSpec, List<Servlet> servlets) throws IOException{
+    if (servlets == null){
+      return;
+    }
+    File bookieApi = new File(webApp);
+    if (!bookieApi.isDirectory()) {
+      Files.createDirectories(bookieApi.toPath());
+    }
+    WebAppContext webAppBookie = new WebAppContext(bookieApi.getAbsolutePath(), contextPath);
+    for (Servlet s:servlets) {
+      webAppBookie.addServlet(new ServletHolder(s), pathSpec);
+    }
+    contexts.addHandler(webAppBookie);
+  }
+
+  /**
+   * Start jetty server.
+   **/
+  public void startServer() throws Exception{
+       jettyServer.start();
+  }
+
+  /**
+   * Stop jetty server.
+   **/
+  public void stopServer() throws Exception{
+       jettyServer.stop();
+  }
+}
diff --git a/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/TestBookieHttpServiceServlet.java b/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/TestBookieHttpServiceServlet.java
new file mode 100644
index 0000000..1591e04
--- /dev/null
+++ b/bookkeeper-http/servlet-http-server/src/test/java/org/apache/bookkeeper/http/servlet/TestBookieHttpServiceServlet.java
@@ -0,0 +1,67 @@
+/**
+ *
+ * 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.bookkeeper.http.servlet;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.Servlet;
+import org.apache.bookkeeper.http.NullHttpServiceProvider;
+import org.apache.commons.io.IOUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+/**
+ * Test bookie http service servlet.
+ **/
+public class TestBookieHttpServiceServlet {
+
+  private JettyHttpServer jettyHttpServer;
+  private String host = "localhost";
+  private int port = 8080;
+  private BookieServletHttpServer bookieServletHttpServer;
+  @Before
+  public void setUp() throws Exception {
+    this.bookieServletHttpServer = new BookieServletHttpServer();
+    this.bookieServletHttpServer.initialize(new NullHttpServiceProvider());
+    this.jettyHttpServer = new JettyHttpServer(host, port);
+    List<Servlet> servlets = new ArrayList<>();
+    servlets.add(new BookieHttpServiceServlet());
+    jettyHttpServer.addServlet("web/bookie", "/", "/",  servlets);
+    jettyHttpServer.startServer();
+  }
+
+  @Test
+  public void testBookieHeartBeat() throws URISyntaxException, IOException {
+    assertThat(IOUtils.toString(new URI(String.format("http://%s:%d/heartbeat", host, port)), "UTF-8"),
+                                                                                  containsString("OK"));
+  }
+
+  @After
+  public void stop() throws Exception{
+    jettyHttpServer.stopServer();
+  }
+}
diff --git a/pom.xml b/pom.xml
index d8eae27..441b4e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,6 +198,7 @@
     <puppycrawl.checkstyle.version>6.19</puppycrawl.checkstyle.version>
     <spotbugs-maven-plugin.version>3.1.8</spotbugs-maven-plugin.version>
     <forkCount.variable>1</forkCount.variable>
+    <servlet-api.version>4.0.0</servlet-api.version>
   </properties>
 
   <!-- dependency definitions -->
@@ -359,6 +360,11 @@
         <version>${jackson.version}</version>
       </dependency>
       <dependency>
+        <groupId>javax.servlet</groupId>
+        <artifactId>javax.servlet-api</artifactId>
+        <version>${servlet-api.version}</version>
+      </dependency>
+      <dependency>
         <groupId>com.fasterxml.jackson.module</groupId>
         <artifactId>jackson-module-paranamer</artifactId>
         <version>${jackson.version}</version>
@@ -574,6 +580,11 @@
       </dependency>
       <dependency>
         <groupId>org.eclipse.jetty</groupId>
+        <artifactId>jetty-webapp</artifactId>
+        <version>${jetty.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.eclipse.jetty</groupId>
         <artifactId>jetty-servlet</artifactId>
         <version>${jetty.version}</version>
       </dependency>
@@ -624,7 +635,7 @@
         <artifactId>sketches-core</artifactId>
         <version>${datasketches.version}</version>
       </dependency>
-      
+
       <!-- http-client -->
       <dependency>
         <groupId>org.apache.httpcomponents</groupId>