You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sk...@apache.org on 2021/03/22 21:29:38 UTC
[ignite-3] branch main updated: IGNITE-14202 Netty-based REST API
sub-library for Ignite. Fixes #71
This is an automated email from the ASF dual-hosted git repository.
sk0x50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new d9e88e9 IGNITE-14202 Netty-based REST API sub-library for Ignite. Fixes #71
d9e88e9 is described below
commit d9e88e9e8972f1a572775223e991262143b17638
Author: Kirill Gusakov <kg...@gmail.com>
AuthorDate: Tue Mar 23 00:29:03 2021 +0300
IGNITE-14202 Netty-based REST API sub-library for Ignite. Fixes #71
Signed-off-by: Slava Koptilin <sl...@gmail.com>
---
modules/rest/pom.xml | 42 +++++-
.../java/org/apache/ignite/rest/RestModule.java | 161 +++++++++++++--------
.../apache/ignite/rest/netty/RestApiHandler.java | 112 ++++++++++++++
.../ignite/rest/netty/RestApiHttpRequest.java | 60 ++++++++
.../ignite/rest/netty/RestApiHttpResponse.java | 134 +++++++++++++++++
.../ignite/rest/netty/RestApiInitializer.java | 50 +++++++
.../java/org/apache/ignite/rest/routes/Route.java | 152 +++++++++++++++++++
.../java/org/apache/ignite/rest/routes/Router.java | 126 ++++++++++++++++
.../org/apache/ignite/rest/routes/RouteTest.java | 97 +++++++++++++
modules/runner/pom.xml | 5 -
.../java/org/apache/ignite/app/IgniteRunner.java | 2 +-
.../src/main/resources/simplelogger.properties | 2 +-
parent/pom.xml | 38 ++++-
13 files changed, 900 insertions(+), 81 deletions(-)
diff --git a/modules/rest/pom.xml b/modules/rest/pom.xml
index f9fb1c4..8cf8376 100644
--- a/modules/rest/pom.xml
+++ b/modules/rest/pom.xml
@@ -47,11 +47,6 @@
</dependency>
<dependency>
- <groupId>io.javalin</groupId>
- <artifactId>javalin</artifactId>
- </dependency>
-
- <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
@@ -62,6 +57,43 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-common</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-buffer</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-handler</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-http</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
index 5d6a270..6124abd 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
@@ -18,18 +18,30 @@
package org.apache.ignite.rest;
import com.google.gson.JsonSyntaxException;
-import io.javalin.Javalin;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
import java.io.Reader;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
+import java.util.Map;
import org.apache.ignite.configuration.ConfigurationRegistry;
import org.apache.ignite.configuration.validation.ConfigurationValidationException;
import org.apache.ignite.rest.configuration.RestConfiguration;
+import org.apache.ignite.rest.netty.RestApiInitializer;
import org.apache.ignite.rest.presentation.ConfigurationPresentation;
-import org.apache.ignite.rest.presentation.FormatConverter;
-import org.apache.ignite.rest.presentation.json.JsonConverter;
import org.apache.ignite.rest.presentation.json.JsonPresentation;
+import org.apache.ignite.rest.routes.Router;
import org.slf4j.Logger;
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+
/**
* Rest module is responsible for starting a REST endpoints for accessing and managing configuration.
*
@@ -61,8 +73,8 @@ public class RestModule {
}
/** */
- public void prepareStart(ConfigurationRegistry sysConfig, Reader moduleConfReader) {
- sysConf = sysConfig;
+ public void prepareStart(ConfigurationRegistry sysCfg, Reader moduleConfReader) {
+ sysConf = sysCfg;
presentation = new JsonPresentation(Collections.emptyMap());
@@ -74,68 +86,77 @@ public class RestModule {
// sysConfig.registerConfigurator(restConf);
}
- /** */
- public void start() {
- Javalin app = startRestEndpoint();
-
- FormatConverter converter = new JsonConverter();
-
- app.get(CONF_URL, ctx -> {
- ctx.result(presentation.represent());
- });
-
- app.get(CONF_URL + ":" + PATH_PARAM, ctx -> {
- String configPath = ctx.pathParam(PATH_PARAM);
-
- try {
- ctx.result(presentation.representByPath(configPath));
- }
- catch (IllegalArgumentException pathE) {
- ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", pathE.getMessage());
-
- ctx.status(400).result(converter.convertTo("error", eRes));
- }
- });
+ /**
+ *
+ */
+ public void start() throws InterruptedException {
+ var router = new Router();
+ router
+ .get(CONF_URL, (req, resp) -> {
+ resp.json(presentation.represent());
+ })
+ .get(CONF_URL + ":" + PATH_PARAM, (req, resp) -> {
+ String cfgPath = req.queryParams().get(PATH_PARAM);
+ try {
+ resp.json(presentation.representByPath(cfgPath));
+ }
+ catch (IllegalArgumentException pathE) {
+ ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", pathE.getMessage());
- app.post(CONF_URL, ctx -> {
- try {
- presentation.update(ctx.body());
- }
- catch (IllegalArgumentException argE) {
- ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", argE.getMessage());
+ resp.status(BAD_REQUEST);
+ resp.json(Map.of("error", eRes));
+ }
+ })
+ .put(CONF_URL, HttpHeaderValues.APPLICATION_JSON, (req, resp) -> {
+ try {
+ presentation.update(
+ req
+ .request()
+ .content()
+ .readCharSequence(req.request().content().readableBytes(), StandardCharsets.UTF_8)
+ .toString());
+ }
+ catch (IllegalArgumentException argE) {
+ ErrorResult eRes = new ErrorResult("CONFIG_PATH_UNRECOGNIZED", argE.getMessage());
- ctx.status(400).result(converter.convertTo("error", eRes));
- }
- catch (ConfigurationValidationException validationE) {
- ErrorResult eRes = new ErrorResult("APPLICATION_EXCEPTION", validationE.getMessage());
+ resp.status(BAD_REQUEST);
+ resp.json(Map.of("error", eRes));
+ }
+ catch (ConfigurationValidationException validationE) {
+ ErrorResult eRes = new ErrorResult("APPLICATION_EXCEPTION", validationE.getMessage());
- ctx.status(400).result(converter.convertTo("error", eRes));
- }
- catch (JsonSyntaxException e) {
- String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
+ resp.status(BAD_REQUEST);
+ resp.json(Map.of("error", eRes));
+ resp.json(eRes);
+ }
+ catch (JsonSyntaxException e) {
+ String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
- ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", msg);
+ ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", msg);
+ resp.status(BAD_REQUEST);
+ resp.json(Map.of("error", eRes));
+ }
+ catch (Exception e) {
+ ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", e.getMessage());
- ctx.status(400).result(converter.convertTo("error", eRes));
- }
- catch (Exception e) {
- ErrorResult eRes = new ErrorResult("VALIDATION_EXCEPTION", e.getMessage());
+ resp.status(BAD_REQUEST);
+ resp.json(Map.of("error", eRes));
+ }
+ });
- ctx.status(400).result(converter.convertTo("error", eRes));
- }
- });
+ startRestEndpoint(router);
}
/** */
- private Javalin startRestEndpoint() {
- Integer port = sysConf.getConfiguration(RestConfiguration.KEY).port().value();
+ private void startRestEndpoint(Router router) throws InterruptedException {
+ Integer desiredPort = sysConf.getConfiguration(RestConfiguration.KEY).port().value();
Integer portRange = sysConf.getConfiguration(RestConfiguration.KEY).portRange().value();
- Javalin app = null;
+ int port = 0;
if (portRange == null || portRange == 0) {
try {
- app = Javalin.create().start(port != null ? port : DFLT_PORT);
+ port = (desiredPort != null ? desiredPort : DFLT_PORT);
}
catch (RuntimeException e) {
log.warn("Failed to start REST endpoint: ", e);
@@ -144,23 +165,20 @@ public class RestModule {
}
}
else {
- int startPort = port;
+ int startPort = desiredPort;
for (int portCandidate = startPort; portCandidate < startPort + portRange; portCandidate++) {
try {
- app = Javalin.create().start(portCandidate);
+ port = (portCandidate);
}
catch (RuntimeException ignored) {
// No-op.
}
-
- if (app != null)
- break;
}
- if (app == null) {
+ if (port == 0) {
String msg = "Cannot start REST endpoint. " +
- "All ports in range [" + startPort + ", " + (startPort + portRange) + ") are in use.";
+ "All ports in range [" + startPort + ", " + (startPort + portRange) + "] are in use.";
log.warn(msg);
@@ -168,9 +186,28 @@ public class RestModule {
}
}
- log.info("REST protocol started successfully on port " + app.port());
+ EventLoopGroup bossGrp = new NioEventLoopGroup(1);
+ EventLoopGroup workerGrp = new NioEventLoopGroup();
+ var hnd = new RestApiInitializer(router);
+ try {
+ ServerBootstrap b = new ServerBootstrap();
+ b.option(ChannelOption.SO_BACKLOG, 1024);
+ b.group(bossGrp, workerGrp)
+ .channel(NioServerSocketChannel.class)
+ .handler(new LoggingHandler(LogLevel.INFO))
+ .childHandler(hnd);
+
+ Channel ch = b.bind(port).sync().channel();
- return app;
+ if (log.isInfoEnabled())
+ log.info("REST protocol started successfully on port " + port);
+
+ ch.closeFuture().sync();
+ }
+ finally {
+ bossGrp.shutdownGracefully();
+ workerGrp.shutdownGracefully();
+ }
}
/** */
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java
new file mode 100644
index 0000000..66bb0e0
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHandler.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ignite.rest.netty;
+
+import io.netty.buffer.EmptyByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.DefaultHttpResponse;
+import io.netty.handler.codec.http.EmptyHttpHeaders;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpVersion;
+import org.apache.ignite.rest.routes.Router;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+
+/**
+ * Main handler of REST HTTP chain.
+ * It receives http request, process it by {@link Router} and produce http response.
+ */
+public class RestApiHandler extends SimpleChannelInboundHandler<HttpObject> {
+ /** */
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ /** Requests' router. */
+ private final Router router;
+
+ /**
+ * @param router Router.
+ */
+ public RestApiHandler(Router router) {
+ this.router = router;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void channelReadComplete(ChannelHandlerContext ctx) {
+ ctx.flush();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
+ if (msg instanceof FullHttpRequest) {
+ FullHttpRequest req = (FullHttpRequest) msg;
+ FullHttpResponse res;
+
+ var maybeRoute = router.route(req);
+ if (maybeRoute.isPresent()) {
+ var resp = new RestApiHttpResponse(new DefaultHttpResponse(HttpVersion.HTTP_1_1, OK));
+ maybeRoute.get().handle(req, resp);
+ var content = resp.content() != null ?
+ Unpooled.wrappedBuffer(resp.content()) :
+ new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT);
+ res = new DefaultFullHttpResponse(resp.protocolVersion(), resp.status(),
+ content, resp.headers(), EmptyHttpHeaders.INSTANCE);
+ }
+ else
+ res = new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.NOT_FOUND);
+
+ res.headers()
+ .setInt(CONTENT_LENGTH, res.content().readableBytes());
+
+ boolean keepAlive = HttpUtil.isKeepAlive(req);
+ if (keepAlive) {
+ if (!req.protocolVersion().isKeepAliveDefault())
+ res.headers().set(CONNECTION, KEEP_ALIVE);
+ } else
+ res.headers().set(CONNECTION, CLOSE);
+
+ ChannelFuture f = ctx.write(res);
+
+ if (!keepAlive)
+ f.addListener(ChannelFutureListener.CLOSE);
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ log.error("Failed to process http request:", cause);
+ var res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR);
+ ctx.write(res).addListener(ChannelFutureListener.CLOSE);
+ }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java
new file mode 100644
index 0000000..92c0987
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpRequest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ignite.rest.netty;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * HTTP request wrapper with GET query params if exists.
+ */
+public class RestApiHttpRequest {
+ /** Request. */
+ private final FullHttpRequest req;
+
+ /** Query params. */
+ private final Map<String, String> qryParams;
+
+ /**
+ * @param req Request.
+ * @param qryParams Query params.
+ */
+ public RestApiHttpRequest(FullHttpRequest req, Map<String, String> qryParams) {
+ this.req = req;
+ this.qryParams = Collections.unmodifiableMap(qryParams);
+ }
+
+ /**
+ * Returns complete HTTP request.
+ *
+ * @return Complete HTTP request.
+ */
+ public FullHttpRequest request() {
+ return req;
+ }
+
+ /**
+ * Returns query parameters associated with the request.
+ *
+ * @return Query parameters.
+ */
+ public Map<String, String> queryParams() {
+ return qryParams;
+ }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java
new file mode 100644
index 0000000..7f25103
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiHttpResponse.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ignite.rest.netty;
+
+import com.google.gson.Gson;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpVersion;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Simple wrapper of HTTP response with some helper methods for filling it with headers and content.
+ */
+public class RestApiHttpResponse {
+ /** Response. */
+ private final HttpResponse res;
+
+ /** Content. */
+ private byte[] content;
+
+ /**
+ * Creates a new HTTP response with the given message body.
+ *
+ * @param res Response.
+ * @param content Content.
+ */
+ public RestApiHttpResponse(HttpResponse res, byte[] content) {
+ this.res = res;
+ this.content = content;
+ }
+
+ /**
+ * Creates a new HTTP response with the given headers and status.
+ *
+ * @param res Response.
+ */
+ public RestApiHttpResponse(HttpResponse res) {
+ this.res = res;
+ }
+
+ /**
+ * Set raw bytes as response body.
+ *
+ * @param content Content data.
+ * @return Updated response.
+ */
+ public RestApiHttpResponse content(byte[] content) {
+ this.content = content;
+ return this;
+ }
+
+ /**
+ * Set JSON representation of input object as response body.
+ *
+ * @param content Content object.
+ * @return Updated response.
+ */
+ public RestApiHttpResponse json(Object content) {
+ // TODO: IGNITE-14344 Gson object should not be created on every response
+ this.content = new Gson().toJson(content).getBytes(StandardCharsets.UTF_8);
+ headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON.toString());
+ return this;
+ }
+
+ /**
+ * @return Content.
+ */
+ public byte[] content() {
+ return content;
+ }
+
+ /**
+ * Returns HTTP status of this response.
+ *
+ * @return HTTP Status.
+ */
+ public HttpResponseStatus status() {
+ return res.status();
+ }
+
+ /**
+ * Sets HTTP status.
+ *
+ * @param status Status.
+ * @return Updated response.
+ */
+ public RestApiHttpResponse status(HttpResponseStatus status) {
+ res.setStatus(status);
+ return this;
+ }
+
+ /**
+ * @return Protocol version
+ */
+ public HttpVersion protocolVersion() {
+ return res.protocolVersion();
+ }
+
+ /**
+ * Sets protocol version.
+ *
+ * @param httpVer HTTP version.
+ * @return Updated response.
+ */
+ public RestApiHttpResponse protocolVersion(HttpVersion httpVer) {
+ res.setProtocolVersion(httpVer);
+ return this;
+ }
+
+ /**
+ * @return Mutable response headers.
+ */
+ public HttpHeaders headers() {
+ return res.headers();
+ }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java
new file mode 100644
index 0000000..8ff84fc
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/netty/RestApiInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ignite.rest.netty;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
+import org.apache.ignite.rest.routes.Router;
+
+/**
+ * Initializes channel with needed handlers for HTTP processing.
+ */
+public class RestApiInitializer extends ChannelInitializer<SocketChannel> {
+ /** Router. */
+ private final Router router;
+
+ /**
+ * @param router Router.
+ */
+ public RestApiInitializer(Router router) {
+ this.router = router;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void initChannel(SocketChannel ch) {
+ ChannelPipeline p = ch.pipeline();
+ p.addLast(new HttpServerCodec());
+ p.addLast(new HttpServerExpectContinueHandler());
+ p.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
+ p.addLast(new RestApiHandler(router));
+ }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java
new file mode 100644
index 0000000..df4f36d
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Route.java
@@ -0,0 +1,152 @@
+/*
+ * 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.ignite.rest.routes;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.apache.ignite.rest.netty.RestApiHttpRequest;
+import org.apache.ignite.rest.netty.RestApiHttpResponse;
+
+/**
+ * URI route with appropriate handler for request.
+ */
+public class Route {
+ /** Route. */
+ private final String route;
+
+ /** Method. */
+ private final HttpMethod method;
+
+ /** Accept type. */
+ private final String acceptType;
+
+ /** Handler. */
+ private final BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd;
+
+ /**
+ * @param route Route.
+ * @param method Method.
+ * @param acceptType Accept type.
+ * @param hnd Request handler.
+ */
+ public Route(
+ String route,
+ HttpMethod method,
+ String acceptType,
+ BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+ ) {
+ this.route = route;
+ this.method = method;
+ this.acceptType = acceptType;
+ this.hnd = hnd;
+ }
+
+ /**
+ * Handles the query by populating the received response object.
+ *
+ * @param req Request.
+ * @param resp Response.
+ */
+ public void handle(FullHttpRequest req, RestApiHttpResponse resp) {
+ hnd.accept(new RestApiHttpRequest(req, paramsDecode(req.uri())), resp);
+ }
+
+ /**
+ * Checks if the current route matches the request.
+ *
+ * @param req Request.
+ * @return true if route matches the request, else otherwise.
+ */
+ public boolean match(HttpRequest req) {
+ return req.method().equals(method) &&
+ matchUri(req.uri()) &&
+ matchContentType(req.headers().get(HttpHeaderNames.CONTENT_TYPE));
+ }
+
+ /**
+ * @param s Content type.
+ * @return true if route matches the request, else otherwise.
+ */
+ private boolean matchContentType(String s) {
+ return (acceptType == null) || (acceptType.equals(s));
+ }
+
+ /**
+ * Checks the current route matches input uri.
+ * REST API like URIs "/user/:user" is also supported.
+ *
+ * @param uri Input URI
+ * @return true if route matches the request, else otherwise.
+ */
+ private boolean matchUri(String uri) {
+ var receivedParts = new ArrayDeque<>(Arrays.asList(uri.split("/")));
+ var realParts = new ArrayDeque<>(Arrays.asList(route.split("/")));
+
+ String part;
+ while ((part = realParts.pollFirst()) != null) {
+ String receivedPart = receivedParts.pollFirst();
+ if (receivedPart == null)
+ return false;
+
+ if (part.startsWith(":"))
+ continue;
+
+ if (!part.equals(receivedPart))
+ return false;
+ }
+
+ return receivedParts.isEmpty();
+ }
+
+ /**
+ * Decodes params from REST like URIs "/user/:user".
+ *
+ * @param uri Input URI.
+ * @return Map of decoded params.
+ */
+ private Map<String, String> paramsDecode(String uri) {
+ var receivedParts = new ArrayDeque<>(Arrays.asList(uri.split("/")));
+ var realParts = new ArrayDeque<>(Arrays.asList(route.split("/")));
+
+ Map<String, String> res = new HashMap<>();
+
+ String part;
+ while ((part = realParts.pollFirst()) != null) {
+ String receivedPart = receivedParts.pollFirst();
+ if (receivedPart == null)
+ throw new IllegalArgumentException("URI is incorrect");
+
+ if (part.startsWith(":")) {
+ res.put(part.substring(1), receivedPart);
+ continue;
+ }
+
+ if (!part.equals(receivedPart))
+ throw new IllegalArgumentException("URI is incorrect");
+ }
+
+ return res;
+ }
+}
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java
new file mode 100644
index 0000000..22869b8
--- /dev/null
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/routes/Router.java
@@ -0,0 +1,126 @@
+/*
+ * 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.ignite.rest.routes;
+
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.AsciiString;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import org.apache.ignite.rest.netty.RestApiHttpRequest;
+import org.apache.ignite.rest.netty.RestApiHttpResponse;
+
+/**
+ * Dispatcher of http requests.
+ *
+ * Example:
+ * <pre>
+ * {@code
+ * var router = new Router();
+ * router.get("/user", (req, resp) -> {
+ * resp.status(HttpResponseStatus.OK);
+ * });
+ * }
+ * </pre>
+ */
+public class Router {
+ /** Routes. */
+ private/*public*/ final List<Route> routes;
+
+ /**
+ * Creates a new router with the given list of {@code routes}.
+ *
+ * @param routes Routes.
+ */
+ public Router(List<Route> routes) {
+ this.routes = routes;
+ }
+
+ /**
+ * Creates a new empty router.
+ */
+ public Router() {
+ routes = new ArrayList<>();
+ }
+
+ /**
+ * GET query helper.
+ *
+ * @param route Route.
+ * @param acceptType Accept type.
+ * @param hnd Actual handler of the request.
+ * @return Router
+ */
+ public Router get(
+ String route,
+ AsciiString acceptType,
+ BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+ ) {
+ addRoute(new Route(route, HttpMethod.GET, acceptType.toString(), hnd));
+ return this;
+ }
+
+ /**
+ * GET query helper.
+ *
+ * @param route Route.
+ * @param hnd Actual handler of the request.
+ * @return Router
+ */
+ public Router get(String route, BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd) {
+ addRoute(new Route(route, HttpMethod.GET, null, hnd));
+ return this;
+ }
+
+ /**
+ * PUT query helper.
+ *
+ * @param route Route.
+ * @param hnd Actual handler of the request.
+ * @return Router
+ */
+ public Router put(
+ String route,
+ AsciiString acceptType,
+ BiConsumer<RestApiHttpRequest, RestApiHttpResponse> hnd
+ ) {
+ addRoute(new Route(route, HttpMethod.PUT, acceptType.toString(), hnd));
+ return this;
+ }
+
+ /**
+ * Adds the route to router chain.
+ *
+ * @param route Route
+ */
+ public void addRoute(Route route) {
+ routes.add(route);
+ }
+
+ /**
+ * Finds the route by request.
+ *
+ * @param req Request.
+ * @return Route if founded.
+ */
+ public Optional<Route> route(HttpRequest req) {
+ return routes.stream().filter(r -> r.match(req)).findFirst();
+ }
+}
diff --git a/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java b/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java
new file mode 100644
index 0000000..dab6573
--- /dev/null
+++ b/modules/rest/src/test/java/org/apache/ignite/rest/routes/RouteTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.ignite.rest.routes;
+
+import io.netty.handler.codec.http.DefaultHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import org.junit.jupiter.api.Test;
+
+import static io.netty.handler.codec.http.HttpMethod.GET;
+import static io.netty.handler.codec.http.HttpMethod.PUT;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ *
+ */
+public class RouteTest {
+ /**
+ *
+ */
+ @Test
+ void testMatchByUri() {
+ var route = new Route("/user", GET, null, (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user");
+ assertTrue(route.match(req));
+ }
+
+ /**
+ *
+ */
+ @Test
+ void testNonMatchByUri() {
+ var route = new Route("/user", GET, null, (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/1");
+ assertFalse(route.match(req));
+ }
+
+ /**
+ *
+ */
+ @Test
+ void testMatchByContentTypeIfAcceptTypeEmpty() {
+ var route = new Route("/user", GET, null, (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/");
+ req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+ assertTrue(route.match(req));
+ }
+
+ /**
+ *
+ */
+ @Test
+ void testMatchByContentTypeIfAcceptTypeNonEmpty() {
+ var route = new Route("/user", PUT, "text/plain", (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, PUT, "/user");
+ req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+ assertTrue(route.match(req));
+ }
+
+ /**
+ *
+ */
+ @Test
+ void testNonMatchByContentTypeIfAcceptTypeNonEmpty() {
+ var route = new Route("/user", PUT, "text/plain", (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/");
+ req.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
+ assertFalse(route.match(req));
+ }
+
+ /**
+ *
+ */
+ @Test
+ void testMatchByUriWithParams() {
+ var route = new Route("/user/:user", GET, null, (request, response) -> {});
+ var req = new DefaultHttpRequest(HTTP_1_1, GET, "/user/John");
+ assertTrue(route.match(req));
+ }
+}
diff --git a/modules/runner/pom.xml b/modules/runner/pom.xml
index af05830..2a2af71 100644
--- a/modules/runner/pom.xml
+++ b/modules/runner/pom.xml
@@ -62,11 +62,6 @@
</dependency>
<dependency>
- <groupId>io.javalin</groupId>
- <artifactId>javalin</artifactId>
- </dependency>
-
- <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
diff --git a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
index 3e5aaf8..007592a 100644
--- a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
+++ b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
@@ -70,7 +70,7 @@ public class IgniteRunner {
*
* @param args Empty or providing path to custom configuration file after marker parameter "--config".
*/
- public static void main(String[] args) throws IOException {
+ public static void main(String[] args) throws IOException, InterruptedException {
ackBanner();
ConfigurationModule confModule = new ConfigurationModule();
diff --git a/modules/runner/src/main/resources/simplelogger.properties b/modules/runner/src/main/resources/simplelogger.properties
index 4d52a62..77a2ff2 100644
--- a/modules/runner/src/main/resources/simplelogger.properties
+++ b/modules/runner/src/main/resources/simplelogger.properties
@@ -16,4 +16,4 @@
#
org.slf4j.simpleLogger.defaultLogLevel=off
-org.slf4j.simpleLogger.log.org.apache.ignite.app.IgniteRunner=info
\ No newline at end of file
+org.slf4j.simpleLogger.log.org.apache.ignite.app.IgniteRunner=info
diff --git a/parent/pom.xml b/parent/pom.xml
index 3eeee99..2616325 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -56,7 +56,7 @@
<gson.version>2.8.6</gson.version>
<jackson.databind.version>2.11.1</jackson.databind.version>
<jansi.version>1.18</jansi.version>
- <javalin.version>3.12.0</javalin.version>
+ <netty.version>4.1.60.Final</netty.version>
<javapoet.version>1.13.0</javapoet.version>
<javax.annotation.api.version>1.3.2</javax.annotation.api.version>
<javax.validation.version>2.0.1.Final</javax.validation.version>
@@ -187,12 +187,6 @@
</dependency>
<dependency>
- <groupId>io.javalin</groupId>
- <artifactId>javalin</artifactId>
- <version>${javalin.version}</version>
- </dependency>
-
- <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
@@ -288,6 +282,36 @@
<artifactId>asm-util</artifactId>
<version>${asm.framework.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-common</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-buffer</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-handler</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-codec-http</artifactId>
+ <version>${netty.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>