├── .gitignore
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── Sockets.java-application-conventions.gradle.kts
│ ├── Sockets.java-common-conventions.gradle.kts
│ └── Sockets.java-library-conventions.gradle.kts
├── functional-server-app
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── app
│ └── App.java
├── functional-server-library
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── Sockets
│ ├── Server.java
│ ├── contract
│ ├── HttpMethod.java
│ └── RequestRunner.java
│ ├── http
│ ├── HttpDecoder.java
│ └── HttpHandler.java
│ ├── pojos
│ ├── HttpRequest.java
│ ├── HttpResponse.java
│ └── HttpStatusCode.java
│ └── writers
│ └── ResponseWriter.java
├── gradlew
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Gradle project-specific cache directory
2 | .gradle
3 |
4 | # Ignore Gradle build output directory
5 | build
6 | .idea/gradle.xml
7 | */build
8 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * This project uses @Incubating APIs which are subject to change.
5 | */
6 |
7 | plugins {
8 | // Support convention plugins written in Kotlin. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build.
9 | `kotlin-dsl`
10 | }
11 |
12 | repositories {
13 | // Use the plugin portal to apply community plugins in convention plugins.
14 | gradlePluginPortal()
15 | }
16 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Sockets.java-application-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // Apply the common convention plugin for shared build configuration between library and application projects.
3 | id("Sockets.java-common-conventions")
4 |
5 | // Apply the application plugin to add support for building a CLI application in Java.
6 | application
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Sockets.java-common-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // Apply the java Plugin to add support for Java.
3 | java
4 | }
5 |
6 | repositories {
7 | mavenCentral()
8 | }
9 |
10 | testing {
11 | suites {
12 | // Configure the built-in test suite
13 | val test by getting(JvmTestSuite::class) {
14 | // Use JUnit Jupiter test framework
15 | useJUnitJupiter("5.7.2")
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Sockets.java-library-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * This project uses @Incubating APIs which are subject to change.
5 | */
6 |
7 | plugins {
8 | // Apply the common convention plugin for shared build configuration between library and application projects.
9 | id("Sockets.java-common-conventions")
10 |
11 | // Apply the java-library plugin for API and implementation separation.
12 | `java-library`
13 | }
14 |
--------------------------------------------------------------------------------
/functional-server-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("Sockets.java-application-conventions")
3 | }
4 |
5 | dependencies {
6 | implementation(project(":functional-server-library"))
7 | implementation("org.apache.httpcomponents:httpclient:4.5.13")
8 | }
9 |
10 | application {
11 | // Define the main class for the application.
12 | mainClass.set("app.App")
13 | }
14 |
--------------------------------------------------------------------------------
/functional-server-app/src/main/java/app/App.java:
--------------------------------------------------------------------------------
1 | package app;
2 |
3 | import Sockets.Server;
4 | import Sockets.pojos.HttpResponse;
5 | import java.io.IOException;
6 |
7 | import static Sockets.contract.HttpMethod.GET;
8 |
9 | /**
10 | * Test functional server library.
11 | */
12 | public class App {
13 | public static void main(String[] args) throws IOException {
14 | Server myServer = new Server(8080);
15 | myServer.addRoute(GET, "/testOne",
16 | (req) -> new HttpResponse.Builder()
17 | .setStatusCode(200)
18 | .addHeader("Content-Type", "text/html")
19 | .setEntity("
Hello There...
")
20 | .build());
21 | myServer.start();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/functional-server-library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("Sockets.java-library-conventions")
3 | }
4 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/Server.java:
--------------------------------------------------------------------------------
1 | package Sockets;
2 |
3 | import Sockets.contract.HttpMethod;
4 | import Sockets.contract.RequestRunner;
5 | import Sockets.http.HttpHandler;
6 |
7 | import java.io.IOException;
8 | import java.net.ServerSocket;
9 | import java.net.Socket;
10 | import java.util.HashMap;
11 | import java.util.Map;
12 | import java.util.concurrent.Executor;
13 | import java.util.concurrent.Executors;
14 |
15 | /*
16 | * Simple Server: accepts HTTP connections and responds using
17 | * the Java net socket library.
18 | * - Blocking approach ( 1 request per thread )
19 | * - Non-blocking ( Investigate Java NIO - new IO, Netty uses this? )
20 | */
21 | public class Server {
22 |
23 | private final Map routes;
24 | private final ServerSocket socket;
25 | private final Executor threadPool;
26 | private HttpHandler handler;
27 |
28 | public Server(int port) throws IOException {
29 | routes = new HashMap<>();
30 | threadPool = Executors.newFixedThreadPool(100);
31 | socket = new ServerSocket(port);
32 | }
33 |
34 | public void start() throws IOException {
35 | handler = new HttpHandler(routes);
36 |
37 | while (true) {
38 | Socket clientConnection = socket.accept();
39 | handleConnection(clientConnection);
40 | }
41 | }
42 |
43 | /*
44 | * Capture each Request / Response lifecycle in a thread
45 | * executed on the threadPool.
46 | */
47 | private void handleConnection(Socket clientConnection) {
48 | Runnable httpRequestRunner = () -> {
49 | try {
50 | handler.handleConnection(clientConnection.getInputStream(), clientConnection.getOutputStream());
51 | } catch (IOException ignored) {
52 | }
53 | };
54 | threadPool.execute(httpRequestRunner);
55 | }
56 |
57 | public void addRoute(final HttpMethod opCode, final String route, final RequestRunner runner) {
58 | routes.put(opCode.name().concat(route), runner);
59 | }
60 | }
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/contract/HttpMethod.java:
--------------------------------------------------------------------------------
1 | package Sockets.contract;
2 |
3 | public enum HttpMethod {
4 | GET,
5 | PUT,
6 | POST,
7 | PATCH
8 | }
9 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/contract/RequestRunner.java:
--------------------------------------------------------------------------------
1 | package Sockets.contract;
2 |
3 | import Sockets.pojos.HttpRequest;
4 | import Sockets.pojos.HttpResponse;
5 |
6 | public interface RequestRunner {
7 | HttpResponse run(HttpRequest request);
8 | }
9 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/http/HttpDecoder.java:
--------------------------------------------------------------------------------
1 | package Sockets.http;
2 |
3 | import Sockets.contract.HttpMethod;
4 | import Sockets.pojos.HttpRequest;
5 | import Sockets.pojos.HttpRequest.Builder;
6 |
7 | import java.io.InputStream;
8 | import java.io.InputStreamReader;
9 | import java.net.URI;
10 | import java.net.URISyntaxException;
11 | import java.util.*;
12 |
13 | /**
14 | * HttpDecoder:
15 | * InputStreamReader -> bytes to characters ( decoded with certain Charset ( ascii ) )
16 | * BufferedReader -> character stream to text
17 | */
18 | public class HttpDecoder {
19 | public static Optional decode(final InputStream inputStream) {
20 | return readMessage(inputStream).flatMap(HttpDecoder::buildRequest);
21 | }
22 |
23 | private static Optional buildRequest(List message) {
24 | if (message.isEmpty()) {
25 | return Optional.empty();
26 | }
27 |
28 | String firstLine = message.get(0);
29 | String[] httpInfo = firstLine.split(" ");
30 |
31 | if (httpInfo.length != 3) {
32 | return Optional.empty();
33 | }
34 |
35 | String protocolVersion = httpInfo[2];
36 | if (!protocolVersion.equals("HTTP/1.1")) {
37 | return Optional.empty();
38 | }
39 |
40 | try {
41 | Builder requestBuilder = new Builder();
42 | requestBuilder.setHttpMethod(HttpMethod.valueOf(httpInfo[0]));
43 | requestBuilder.setUri(new URI(httpInfo[1]));
44 | return Optional.of(addRequestHeaders(message, requestBuilder));
45 | } catch (URISyntaxException | IllegalArgumentException e) {
46 | return Optional.empty();
47 | }
48 | }
49 | private static Optional> readMessage(final InputStream inputStream) {
50 | try {
51 | if (!(inputStream.available() > 0)) {
52 | return Optional.empty();
53 | }
54 |
55 | final char[] inBuffer = new char[inputStream.available()];
56 | final InputStreamReader inReader = new InputStreamReader(inputStream);
57 | final int read = inReader.read(inBuffer);
58 |
59 | List message = new ArrayList<>();
60 |
61 | try (Scanner sc = new Scanner(new String(inBuffer))) {
62 | while (sc.hasNextLine()) {
63 | String line = sc.nextLine();
64 | message.add(line);
65 | }
66 | }
67 |
68 | return Optional.of(message);
69 | } catch (Exception ignored) {
70 | return Optional.empty();
71 | }
72 | }
73 |
74 | private static HttpRequest addRequestHeaders(final List message, final Builder builder) {
75 | final Map> requestHeaders = new HashMap<>();
76 |
77 | if (message.size() > 1) {
78 | for (int i = 1; i < message.size(); i++) {
79 | String header = message.get(i);
80 | int colonIndex = header.indexOf(':');
81 |
82 | if (! (colonIndex > 0 && header.length() > colonIndex + 1)) {
83 | break;
84 | }
85 |
86 | String headerName = header.substring(0, colonIndex);
87 | String headerValue = header.substring(colonIndex + 1);
88 |
89 | requestHeaders.compute(headerName, (key, values) -> {
90 | if (values != null) {
91 | values.add(headerValue);
92 | } else {
93 | values = new ArrayList<>();
94 | }
95 | return values;
96 | });
97 | }
98 | }
99 |
100 | builder.setRequestHeaders(requestHeaders);
101 | return builder.build();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/http/HttpHandler.java:
--------------------------------------------------------------------------------
1 | package Sockets.http;
2 |
3 | import Sockets.contract.RequestRunner;
4 | import Sockets.pojos.HttpRequest;
5 | import Sockets.pojos.HttpResponse;
6 | import Sockets.writers.ResponseWriter;
7 |
8 | import java.io.*;
9 | import java.util.Map;
10 | import java.util.Optional;
11 |
12 | /**
13 | * Handle HTTP Request Response lifecycle.
14 | */
15 | public class HttpHandler {
16 |
17 | private final Map routes;
18 |
19 | public HttpHandler(final Map routes) {
20 | this.routes = routes;
21 | }
22 | public void handleConnection(final InputStream inputStream, final OutputStream outputStream) throws IOException {
23 | final BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
24 |
25 | Optional request = HttpDecoder.decode(inputStream);
26 | request.ifPresentOrElse((r) -> handleRequest(r, bufferedWriter), () -> handleInvalidRequest(bufferedWriter));
27 |
28 | bufferedWriter.close();
29 | inputStream.close();
30 | }
31 | private void handleInvalidRequest(final BufferedWriter bufferedWriter) {
32 | HttpResponse notFoundResponse = new HttpResponse.Builder().setStatusCode(400).setEntity("Invalid Request...").build();
33 | ResponseWriter.writeResponse(bufferedWriter, notFoundResponse);
34 | }
35 |
36 | private void handleRequest(final HttpRequest request, final BufferedWriter bufferedWriter) {
37 | final String routeKey = request.getHttpMethod().name().concat(request.getUri().getRawPath());
38 |
39 | if (routes.containsKey(routeKey)) {
40 | ResponseWriter.writeResponse(bufferedWriter, routes.get(routeKey).run(request));
41 | } else {
42 | // Not found
43 | ResponseWriter.writeResponse(bufferedWriter, new HttpResponse.Builder().setStatusCode(404).setEntity("Route Not Found...").build());
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/pojos/HttpRequest.java:
--------------------------------------------------------------------------------
1 | package Sockets.pojos;
2 |
3 | import Sockets.contract.HttpMethod;
4 |
5 | import java.net.URI;
6 | import java.util.List;
7 | import java.util.Map;
8 |
9 | public class HttpRequest {
10 | private final HttpMethod httpMethod;
11 | private final URI uri;
12 | private final Map> requestHeaders;
13 | private HttpRequest(HttpMethod opCode,
14 | URI uri,
15 | Map> requestHeaders)
16 | {
17 | this.httpMethod = opCode;
18 | this.uri = uri;
19 | this.requestHeaders = requestHeaders;
20 | }
21 |
22 | public URI getUri() {
23 | return uri;
24 | }
25 |
26 | public HttpMethod getHttpMethod() {
27 | return httpMethod;
28 | }
29 |
30 | public Map> getRequestHeaders() {
31 | return requestHeaders;
32 | }
33 |
34 | public static class Builder {
35 | private HttpMethod httpMethod;
36 | private URI uri;
37 | private Map> requestHeaders;
38 |
39 | public Builder() {
40 | }
41 |
42 | public void setHttpMethod(HttpMethod httpMethod) {
43 | this.httpMethod = httpMethod;
44 | }
45 |
46 | public void setUri(URI uri) {
47 | this.uri = uri;
48 | }
49 |
50 | public void setRequestHeaders(Map> requestHeaders) {
51 | this.requestHeaders = requestHeaders;
52 | }
53 |
54 | public HttpRequest build() {
55 | return new HttpRequest(httpMethod, uri, requestHeaders);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/functional-server-library/src/main/java/Sockets/pojos/HttpResponse.java:
--------------------------------------------------------------------------------
1 | package Sockets.pojos;
2 |
3 | import java.time.ZoneOffset;
4 | import java.time.ZonedDateTime;
5 | import java.time.format.DateTimeFormatter;
6 | import java.util.HashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 | import java.util.Optional;
10 |
11 | public class HttpResponse {
12 | private final Map> responseHeaders;
13 | private final int statusCode;
14 |
15 | private final Optional