Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
*
* 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;

import java.util.Set;
import lombok.Getter;
import org.apache.bookkeeper.http.service.HttpEndpointService;

/**
* A value object that binds an HTTP path to its handler service,
* optionally restricted to specific HTTP methods.
*/
@Getter
public class HttpEndpoint {

private final String path;
private final HttpEndpointService service;
private final Set<HttpServer.Method> methods;

/**
* Create an endpoint that handles all HTTP methods.
*/
public HttpEndpoint(String path, HttpEndpointService service) {
this(path, service, null);
}

/**
* Create an endpoint restricted to the given HTTP methods.
*
* @param methods the set of allowed methods, or null to allow all methods
*/
public HttpEndpoint(String path, HttpEndpointService service, Set<HttpServer.Method> methods) {
this.path = path;
this.service = service;
this.methods = methods;
}

}
Original file line number Diff line number Diff line change
@@ -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;

import java.util.List;

/**
* SPI interface for HTTP endpoint extensions.
* Configured via "httpExtensions" in bk_server.conf.
*
* <p>One extension class can register multiple endpoints.
*
* <p>Usage:
* <ol>
* <li>Implement this interface</li>
* <li>Set httpExtensions=com.example.MyExtension in bk_server.conf</li>
* <li>Put the JAR in BookKeeper's classpath</li>
* </ol>
*
* <p>Simple usage (no BK internals needed):
* <pre>
* public List&lt;HttpEndpoint&gt; getEndpoints(HttpServiceProvider provider) {
* return Arrays.asList(
* new HttpEndpoint("/api/v1/ext/hello",
* request -&gt; new HttpServiceResponse().setBody("hello"))
* );
* }
* </pre>
*
* <p>Advanced usage (access Bookie internals):
* <pre>
* public List&lt;HttpEndpoint&gt; getEndpoints(HttpServiceProvider provider) {
* BKHttpServiceProvider bkProvider = (BKHttpServiceProvider) provider;
* Bookie bookie = bkProvider.getBookieServer().getBookie();
* ...
* }
* </pre>
*/
public interface HttpExtension {

/**
* Return all endpoints to register.
*
* @param provider the HTTP service provider. In BookKeeper, this is
* {@code BKHttpServiceProvider} which provides access to
* {@code BookieServer}, {@code Bookie}, etc. via casting.
*/
List<HttpEndpoint> getEndpoints(HttpServiceProvider provider);
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,12 @@ enum ApiType {
* Check whether the HTTP server is still running.
*/
boolean isRunning();

/**
* Set the HTTP extension class names to be loaded by the server.
*
* @param extensionClasses fully-qualified class names of {@link HttpExtension} implementations
*/
default void setHttpExtensionClasses(String[] extensionClasses) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,26 @@
*/
package org.apache.bookkeeper.http.vertx;

import com.google.common.base.Strings;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Vertx;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.JksOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import java.io.IOException;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import lombok.CustomLog;
import org.apache.bookkeeper.http.HttpEndpoint;
import org.apache.bookkeeper.http.HttpExtension;
import org.apache.bookkeeper.http.HttpRouter;
import org.apache.bookkeeper.http.HttpServer;
import org.apache.bookkeeper.http.HttpServerConfiguration;
Expand All @@ -47,6 +54,7 @@ public class VertxHttpServer implements HttpServer {
private final Vertx vertx;
private boolean isRunning;
private HttpServiceProvider httpServiceProvider;
private String[] httpExtensionClasses;
private int listeningPort = -1;

public VertxHttpServer() {
Expand All @@ -62,6 +70,14 @@ public void initialize(HttpServiceProvider httpServiceProvider) {
this.httpServiceProvider = httpServiceProvider;
}

/**
* Set the HTTP extension class names to load.
*/
@Override
public void setHttpExtensionClasses(String[] extensionClasses) {
this.httpExtensionClasses = extensionClasses;
}

@Override
public boolean startServer(int port) {
return startServer(port, "0.0.0.0");
Expand All @@ -88,6 +104,7 @@ public void bindHandler(String endpoint, VertxAbstractHandler handler) {
}
};
requestRouter.bindAll();
registerExtensions(router);
vertx.deployVerticle(new AbstractVerticle() {
@Override
public void start() throws Exception {
Expand Down Expand Up @@ -130,6 +147,59 @@ public void start() throws Exception {
return false;
}

/**
* Load and register all configured HTTP extensions.
*/
private void registerExtensions(Router router) {
if (httpExtensionClasses == null || httpExtensionClasses.length == 0) {
return;
}
for (String className : httpExtensionClasses) {
if (Strings.isNullOrEmpty(className)) {
continue;
}
try {
Class<? extends HttpExtension> cls =
Class.forName(className.trim()).asSubclass(HttpExtension.class);
HttpExtension ext = cls.getDeclaredConstructor().newInstance();
List<HttpEndpoint> endpoints = ext.getEndpoints(httpServiceProvider);
for (HttpEndpoint endpoint : endpoints) {
String path = endpoint.getPath();
if (path == null || !path.startsWith("/")) {
log.warn().attr("path", path).log(
"Skipping invalid extension path (must be non-null and start with '/')");
continue;
}
log.info().attr("path", path).attr("class", className).log("Loading HTTP extension");
VertxAbstractHandler handler = new VertxAbstractHandler() {
@Override
public void handle(RoutingContext ctx) {
processRequest(endpoint.getService(), ctx);
}
};
Set<HttpServer.Method> methods = endpoint.getMethods();
if (methods == null) {
methods = EnumSet.allOf(HttpServer.Method.class);
}
if (methods.contains(HttpServer.Method.GET)) {
router.get(path).blockingHandler(handler);
}
if (methods.contains(HttpServer.Method.PUT)) {
router.put(path).blockingHandler(handler);
}
if (methods.contains(HttpServer.Method.POST)) {
router.post(path).blockingHandler(handler);
}
if (methods.contains(HttpServer.Method.DELETE)) {
router.delete(path).blockingHandler(handler);
}
}
} catch (ReflectiveOperationException e) {
log.error().exception(e).attr("class", className).log("Failed to load HTTP extension");
}
}
}

@Override
public void stopServer() {
CountDownLatch shutdownLatch = new CountDownLatch(1);
Expand Down
Loading
Loading