diff --git a/src/main/java/com/wintermindset/App.java b/src/main/java/com/wintermindset/App.java index 3b214f0..7a8df11 100644 --- a/src/main/java/com/wintermindset/App.java +++ b/src/main/java/com/wintermindset/App.java @@ -9,10 +9,34 @@ import com.wintermindset.config.ServerConfig; import com.wintermindset.core.Server; +/** + * Application entry point. + * + *
This class is responsible for bootstrapping the HTTP server. + * It performs the following steps:
+ * + *The configuration file is expected to be located in the application + * resources and defines settings such as the server port and the request + * handler implementation.
+ */ public class App { private static final Logger LOGGER = LogManager.getLogger(App.class); + /** + * Main application entry point. + * + *The method loads the server configuration, initializes the server, + * and starts accepting HTTP connections.
+ * + * @param args command-line arguments (not used) + */ public static void main(String[] args) { try { LOGGER.info("Loading server config"); diff --git a/src/main/java/com/wintermindset/config/ConfigReader.java b/src/main/java/com/wintermindset/config/ConfigReader.java index 1d159fc..a46e011 100644 --- a/src/main/java/com/wintermindset/config/ConfigReader.java +++ b/src/main/java/com/wintermindset/config/ConfigReader.java @@ -7,8 +7,41 @@ import com.wintermindset.handler.HandlerFactory; +/** + * Utility class responsible for loading and parsing server configuration + * from resource files. + * + *The configuration file is expected to be located on the application + * classpath and follow a simple {@code key: value} format.
+ * + *Example configuration file:
+ * + *+ * # Server configuration + * port: 8080 + * handler: com.example.MyHandler + *+ * + *
Supported configuration keys:
+ *Lines starting with {@code #} and empty lines are ignored.
+ */ public final class ConfigReader { + /** + * Loads a {@link ServerConfig} instance from a configuration file + * located in the application resources. + * + * @param resourceName resource file name (e.g. {@code server.conf}) + * @return parsed server configuration + * + * @throws IllegalArgumentException if the resource cannot be found + * @throws RuntimeException if parsing fails + */ public static ServerConfig loadFromResources(String resourceName) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); try (InputStream in = cl.getResourceAsStream(resourceName)) { @@ -25,6 +58,17 @@ public static ServerConfig loadFromResources(String resourceName) { } } + + /** + * Parses the configuration content from a reader. + * + *The method reads the configuration line by line and applies + * recognized keys to the {@link ServerConfig} object.
+ * + * @param in input reader + * @return parsed configuration + * @throws IOException if reading fails + */ private static ServerConfig parse(InputStreamReader in) throws IOException { ServerConfig cfg = new ServerConfig(); try (BufferedReader r = new BufferedReader(in)) { @@ -46,6 +90,13 @@ private static ServerConfig parse(InputStreamReader in) throws IOException { return cfg; } + /** + * Applies a configuration key-value pair to the {@link ServerConfig}. + * + * @param cfg configuration object + * @param key configuration key + * @param value configuration value + */ private static void apply(ServerConfig cfg, String key, String value) { switch (key) { case "port" -> cfg.port = Integer.parseInt(value); diff --git a/src/main/java/com/wintermindset/config/ServerConfig.java b/src/main/java/com/wintermindset/config/ServerConfig.java index afa661f..2b70ce5 100644 --- a/src/main/java/com/wintermindset/config/ServerConfig.java +++ b/src/main/java/com/wintermindset/config/ServerConfig.java @@ -2,8 +2,36 @@ import com.wintermindset.handler.Handler; +/** + * Configuration object for the HTTP server. + * + *This class contains basic settings required to start the server, + * such as the listening port and the request {@link Handler} that + * processes incoming HTTP requests.
+ * + *The configuration is typically created during application startup + * and passed to the server instance.
+ * + *Example usage:
+ * + *+ * ServerConfig config = new ServerConfig(); + * config.port = 8080; + * config.handler = new HelloHandler(); + *+ */ public final class ServerConfig { + /** + * Port on which the HTTP server will listen. + * + *
Default value: {@code 8080}.
+ */ public int port = 8080; + + /** + * Application-level request handler responsible for + * processing incoming HTTP requests. + */ public Handler handler; } \ No newline at end of file diff --git a/src/main/java/com/wintermindset/core/Server.java b/src/main/java/com/wintermindset/core/Server.java index 9bbbe2d..f23d178 100644 --- a/src/main/java/com/wintermindset/core/Server.java +++ b/src/main/java/com/wintermindset/core/Server.java @@ -13,17 +13,53 @@ import com.wintermindset.handler.Handler; import com.wintermindset.http.HttpConnection; + +/** + * Minimal HTTP server implementation. + * + *This class is responsible for accepting incoming TCP connections + * and delegating them to {@link HttpConnection} instances for processing.
+ * + *The server uses Java virtual threads (Project Loom) via + * {@link Executors#newVirtualThreadPerTaskExecutor()} to handle + * connections concurrently with a lightweight threading model.
+ * + *Main responsibilities:
+ *The actual request processing logic is delegated to a + * {@link Handler} implementation.
+ */ public final class Server { private final int port; private final Handler handler; private static final Logger LOGGER = LogManager.getLogger(Server.class); + /** + * Creates a new server instance using the provided configuration. + * + * @param config server configuration containing port and handler + */ public Server(ServerConfig config) { this.port = config.port; this.handler = config.handler; } + /** + * Starts the HTTP server. + * + *The method opens a {@link ServerSocket} and continuously accepts + * incoming connections. Each accepted socket is handled by a + * {@link HttpConnection} running in its own virtual thread.
+ * + * @throws IOException if the server socket cannot be created or fails + */ public void start() throws IOException { ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); try (ServerSocket serverSocket = new ServerSocket(port)) { diff --git a/src/main/java/com/wintermindset/handler/CalcHandler.java b/src/main/java/com/wintermindset/handler/CalcHandler.java index d686dfa..8b5404a 100644 --- a/src/main/java/com/wintermindset/handler/CalcHandler.java +++ b/src/main/java/com/wintermindset/handler/CalcHandler.java @@ -9,10 +9,54 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** + * HTTP handler that exposes a simple calculator endpoint. + * + *The handler processes {@code GET} requests sent to the + * {@code /calc} path and performs a basic arithmetic operation + * using query parameters.
+ * + *Supported query parameters:
+ *Supported operators:
+ *Example request:
+ * + *+ * GET /calc?x=10&y=5&op=+ + *+ * + *
Response:
+ * + *+ * 15.0 + *+ */ public final class CalcHandler implements Handler { private static final Logger LOGGER = LogManager.getLogger(); + /** + * Processes an incoming HTTP request. + * + *
The handler validates the request method and path, + * extracts query parameters, performs the requested + * arithmetic operation, and returns the result.
+ * + * @param req parsed HTTP request + * @return HTTP response containing the calculation result + */ public HttpResponse handle(HttpRequest req) { if (!"GET".equals(req.method)) { return HttpResponse.badRequest("Only GET supported"); @@ -43,6 +87,31 @@ public HttpResponse handle(HttpRequest req) { } } + /** + * Parses query parameters from the request path. + * + *The method extracts the query string portion of the URL + * and converts it into a map of key-value pairs.
+ * + *Example:
+ * + *+ * /calc?x=10&y=5&op=+ + *+ * + * becomes: + * + *
+ * {
+ * x=10,
+ * y=5,
+ * op=+
+ * }
+ *
+ *
+ * @param path request path containing the query string
+ * @return map of parsed query parameters
+ */
private MapA {@code Handler} processes an incoming {@link HttpRequest} + * and produces a corresponding {@link HttpResponse}. Implementations + * typically contain the application logic of the server.
+ * + *Handlers are invoked by the HTTP server infrastructure + * (e.g. a connection handler) after a request has been parsed.
+ * + *Example implementation:
+ * + *
+ * public class HelloHandler implements Handler {
+ * public HttpResponse handle(HttpRequest req) {
+ * return HttpResponse.ok("Hello world");
+ * }
+ * }
+ *
+ */
public interface Handler {
-
+
+ /**
+ * Processes an HTTP request and produces a response.
+ *
+ * @param req parsed HTTP request
+ * @return HTTP response to send back to the client
+ */
HttpResponse handle(HttpRequest req);
}
\ No newline at end of file
diff --git a/src/main/java/com/wintermindset/handler/HandlerFactory.java b/src/main/java/com/wintermindset/handler/HandlerFactory.java
index a075ef4..03a472b 100644
--- a/src/main/java/com/wintermindset/handler/HandlerFactory.java
+++ b/src/main/java/com/wintermindset/handler/HandlerFactory.java
@@ -1,7 +1,42 @@
package com.wintermindset.handler;
+/**
+ * Factory responsible for creating {@link Handler} instances
+ * using reflection.
+ *
+ * This class allows dynamic loading of request handlers + * by their fully qualified class name. The target class must:
+ * + *Typical usage:
+ * + *
+ * Handler handler =
+ * HandlerFactory.fromClassName("com.example.MyHandler");
+ *
+ *
+ * This approach is commonly used for simple plugin-like + * architectures where handlers are configured externally + * (e.g. via configuration files).
+ */ public final class HandlerFactory { + /** + * Creates a {@link Handler} instance from a class name. + * + *The method loads the class using {@link Class#forName(String)} + * and instantiates it using its default constructor.
+ * + * @param className fully qualified handler class name + * @return instantiated handler + * + * @throws IllegalArgumentException if the class does not implement {@link Handler} + * @throws RuntimeException if the class cannot be loaded or instantiated + */ public static Handler fromClassName(String className) { try { Class> clazz = Class.forName(className); diff --git a/src/main/java/com/wintermindset/http/HttpConnection.java b/src/main/java/com/wintermindset/http/HttpConnection.java index b1343b2..a9432c6 100644 --- a/src/main/java/com/wintermindset/http/HttpConnection.java +++ b/src/main/java/com/wintermindset/http/HttpConnection.java @@ -7,6 +7,28 @@ import java.io.IOException; import java.net.Socket; +/** + * Represents a single HTTP connection with a client. + * + *This class is responsible for the full lifecycle of request handling + * over a TCP socket. It reads incoming HTTP requests, delegates processing + * to a {@link Handler}, and writes HTTP responses back to the client.
+ * + *The connection supports persistent HTTP/1.1 connections (keep-alive) + * and may process multiple requests sequentially over the same socket.
+ * + *Typical workflow:
+ *If a parsing error occurs, a {@code 400 Bad Request} response is sent. + * If the handler throws an exception, a {@code 500 Internal Server Error} + * response is returned.
+ */ public final class HttpConnection implements Runnable { private final Socket socket; @@ -15,6 +37,13 @@ public final class HttpConnection implements Runnable { private final BufferedOutput out; private final HttpRequestParser parser = new HttpRequestParser(); + /** + * Creates a new HTTP connection handler. + * + * @param socket client socket + * @param handler request handler + * @throws IOException if socket streams cannot be initialized + */ public HttpConnection(Socket socket, Handler handler) throws IOException { this.socket = socket; this.handler = handler; @@ -22,6 +51,20 @@ public HttpConnection(Socket socket, Handler handler) throws IOException { this.out = new BufferedOutput(socket.getOutputStream()); } + /** + * Main connection loop. + * + *This method processes HTTP requests sequentially until the + * connection is closed or the client disables keep-alive.
+ * + *Execution steps:
+ *This class stores the basic components of an HTTP request: + * the request line, headers, and optional message body.
+ * + *The request is typically created by {@link HttpRequestParser} + * after parsing raw bytes received from a client connection.
+ * + *Example HTTP request:
+ * + *+ * GET /index.html HTTP/1.1 + * Host: example.com + * Connection: keep-alive + *+ */ public final class HttpRequest { + /** HTTP method (e.g. GET, POST, PUT, DELETE). */ public String method; + + /** Request path or URI (e.g. "/index.html"). */ public String path; + + /** HTTP protocol version (e.g. "HTTP/1.1"). */ public String version; + + /** Map of HTTP headers. */ public final Map
Rules implemented:
+ *This class reads raw bytes from {@link BufferedInput} and converts them + * into a {@link HttpRequest} object. The parser processes:
+ * + *The implementation is intentionally minimal and intended for educational + * purposes (e.g. implementing a simple HTTP server).
+ * + *Limitations:
+ *Content-LengthThe method reads:
+ *Example request line:
+ * + *+ * GET /index.html HTTP/1.1 + *+ * + * @param slice byte slice containing the request line + * @param req request object to populate + * @throws IllegalArgumentException if the request line format is invalid + */ private void parseRequestLine(ByteSlice slice, HttpRequest req) { int p1 = indexOf(slice, (byte) ' '); int p2 = indexOf(slice, (byte) ' ', p1 + 1); @@ -42,6 +91,18 @@ private void parseRequestLine(ByteSlice slice, HttpRequest req) { req.version = ascii(slice, p2 + 1, slice.length - p2 - 1); } + /** + * Parses a single HTTP header line and stores it in the request. + * + *
Example header:
+ * + *+ * Content-Type: application/json + *+ * + * @param slice byte slice containing the header line + * @param req request object to update + */ private void parseHeader(ByteSlice slice, HttpRequest req) { int colon = indexOf(slice, (byte) ':'); if (colon <= 0) { @@ -52,15 +113,37 @@ private void parseHeader(ByteSlice slice, HttpRequest req) { req.headers.put(name, value); } + /** + * Extracts the {@code Content-Length} header value. + * + * @param req parsed request + * @return body length in bytes, or {@code 0} if the header is not present + */ private int getContentLength(HttpRequest req) { String v = req.headers.get("content-length"); return v == null ? 0 : Integer.parseInt(v); } + /** + * Finds the first occurrence of a byte in the slice. + * + * @param slice source byte slice + * @param b byte to search for + * @return index of the byte or {@code -1} if not found + */ private int indexOf(ByteSlice slice, byte b) { return indexOf(slice, b, 0); } + /** + * Finds the first occurrence of a byte in the slice starting + * from the specified position. + * + * @param slice source byte slice + * @param b byte to search for + * @param from starting position + * @return index of the byte or {@code -1} if not found + */ private int indexOf(ByteSlice slice, byte b, int from) { for (int i = from; i < slice.length; i++) { if (slice.data[slice.offset + i] == b) { @@ -70,6 +153,17 @@ private int indexOf(ByteSlice slice, byte b, int from) { return -1; } + /** + * Converts a region of a byte slice to a {@link String}. + * + *
Used for decoding ASCII parts of the HTTP request such as + * method, path, version and header fields.
+ * + * @param s source slice + * @param off offset inside the slice + * @param len number of bytes + * @return decoded string + */ private String ascii(ByteSlice s, int off, int len) { return new String(s.data, s.offset + off, len); } diff --git a/src/main/java/com/wintermindset/http/HttpResponse.java b/src/main/java/com/wintermindset/http/HttpResponse.java index e08ac49..3062941 100644 --- a/src/main/java/com/wintermindset/http/HttpResponse.java +++ b/src/main/java/com/wintermindset/http/HttpResponse.java @@ -7,6 +7,23 @@ import com.wintermindset.io.BufferedOutput; +/** + * Represents an HTTP/1.1 response. + * + *This class is responsible for building and serializing HTTP responses. + * It stores the status line, headers and optional response body and provides + * helper methods for writing the response to a {@link BufferedOutput}.
+ * + *Typical usage:
+ * + *
+ * HttpResponse resp = HttpResponse.ok("Hello world");
+ * resp.writeTo(out, true);
+ *
+ *
+ * The implementation is intentionally minimal and designed for a small + * custom HTTP server.
+ */ public final class HttpResponse { private int status = 200; @@ -14,32 +31,85 @@ public final class HttpResponse { private byte[] body = new byte[0]; private final MapThis method also automatically sets the + * {@code Content-Type: text/plain; charset=utf-8} header.
+ * + * @param body response body string + * @return current response instance + */ public HttpResponse body(String body) { this.body = body.getBytes(StandardCharsets.UTF_8); header("Content-Type", "text/plain; charset=utf-8"); return this; } + /** + * Sets a binary response body. + * + * @param body response body bytes + * @return current response instance + */ public HttpResponse body(byte[] body) { this.body = body; return this; } + /** + * Adds or replaces an HTTP header. + * + * @param name header name + * @param value header value + * @return current response instance + */ public HttpResponse header(String name, String value) { headers.put(name, value); return this; } + /** + * Writes the HTTP response to the provided output. + * + *This method serializes the full HTTP message including:
+ *The method automatically sets:
+ *This class wraps an {@link InputStream} and provides efficient methods + * for reading HTTP request components such as CRLF-terminated lines and + * fixed-length bodies.
+ * + *The implementation minimizes object allocations by returning lightweight + * {@link ByteSlice} views over the internal buffer instead of copying data + * when reading header lines.
+ * + *Main features:
+ *The returned slice references the internal buffer and does not + * allocate additional memory.
+ * + * @return slice representing the line (without CRLF) + * @throws IOException if the line exceeds {@link #MAX_HEADER_SIZE} + * or the connection is closed + */ public ByteSlice readLineSlice() throws IOException { while (true) { for (int i = scanPos; i + 1 < limit; i++) { @@ -43,11 +84,25 @@ public ByteSlice readLineSlice() throws IOException { } } + /** + * Reads a CRLF-terminated line and converts it to an ASCII string. + * + * @return decoded string + * @throws IOException if reading fails + */ public String readLine() throws IOException { ByteSlice slice = readLineSlice(); return slice.toStringAscii(); } + /** + * Reads a fixed-length HTTP body. + * + * @param length number of bytes to read + * @return body bytes + * @throws IOException if the body exceeds {@link #MAX_BODY_SIZE} + * or the connection is closed + */ public byte[] readBody(int length) throws IOException { if (length > MAX_BODY_SIZE) { throw new IOException("HTTP body too large"); @@ -66,6 +121,12 @@ public byte[] readBody(int length) throws IOException { return body; } + /** + * Fills the internal buffer with additional data from the stream. + * + * @throws IOException if the client closes the connection or the request + * exceeds allowed size limits + */ private void fill() throws IOException { if (position > 0) { compact(); @@ -80,6 +141,10 @@ private void fill() throws IOException { } } + /** + * Compacts the buffer by moving unread data to the beginning. + * This frees space for additional reads. + */ private void compact() { int remaining = limit - position; System.arraycopy(buffer, position, buffer, 0, remaining); @@ -88,10 +153,21 @@ private void compact() { scanPos = 0; } + /** + * Closes the underlying input stream. + * + * @throws IOException if closing fails + */ public void close() throws IOException { in.close(); } + /** + * Lightweight immutable view over a portion of a byte array. + * + *This class avoids copying data by referencing the original buffer + * along with an offset and length.
+ */ public static final class ByteSlice { public final byte[] data; public final int offset; @@ -103,6 +179,11 @@ public static final class ByteSlice { this.length = length; } + /** + * Converts the slice to an ASCII string. + * + * @return decoded string + */ public String toStringAscii() { return new String(data, offset, length, StandardCharsets.US_ASCII); } diff --git a/src/main/java/com/wintermindset/io/BufferedOutput.java b/src/main/java/com/wintermindset/io/BufferedOutput.java index 6f88c44..2ca1dea 100644 --- a/src/main/java/com/wintermindset/io/BufferedOutput.java +++ b/src/main/java/com/wintermindset/io/BufferedOutput.java @@ -4,6 +4,26 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; +/** + * Buffered writer optimized for HTTP response serialization. + * + *This class wraps an {@link OutputStream} and provides buffered + * write operations to reduce the number of system calls when sending + * data over a network connection.
+ * + *The buffer accumulates written bytes and flushes them to the + * underlying stream when the buffer becomes full or when + * {@link #flush()} is explicitly called.
+ * + *Typical usage:
+ * + *
+ * BufferedOutput out = new BufferedOutput(socket.getOutputStream());
+ * out.writeAscii("HTTP/1.1 200 OK");
+ * out.writeCRLF();
+ * out.flush();
+ *
+ */
public final class BufferedOutput {
private static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
@@ -12,19 +32,47 @@ public final class BufferedOutput {
private final byte[] buffer;
private int position = 0;
+ /**
+ * Creates a buffered output wrapper with the default buffer size.
+ *
+ * @param out underlying output stream
+ */
public BufferedOutput(OutputStream out) {
this(out, DEFAULT_BUFFER_SIZE);
}
+ /**
+ * Creates a buffered output wrapper with a custom buffer size.
+ *
+ * @param out underlying output stream
+ * @param bufferSize size of the internal buffer
+ */
public BufferedOutput(OutputStream out, int bufferSize) {
this.out = out;
this.buffer = new byte[bufferSize];
}
+ /**
+ * Writes the entire byte array to the buffer.
+ *
+ * @param data data to write
+ * @throws IOException if writing fails
+ */
public void write(byte[] data) throws IOException {
write(data, 0, data.length);
}
+ /**
+ * Writes a portion of a byte array to the buffer.
+ *
+ * If the buffer becomes full, it is automatically flushed to the + * underlying stream.
+ * + * @param data source byte array + * @param off starting offset + * @param len number of bytes to write + * @throws IOException if writing fails + */ public void write(byte[] data, int off, int len) throws IOException { int offset = off; while (len > 0) { @@ -41,23 +89,52 @@ public void write(byte[] data, int off, int len) throws IOException { } } + /** + * Writes an ASCII string to the buffer. + * + * @param s ASCII string + * @throws IOException if writing fails + */ public void writeAscii(String s) throws IOException { write(s.getBytes(StandardCharsets.US_ASCII)); } + /** + * Writes a CRLF sequence ({@code \r\n}). + * + *This sequence is used as a line terminator in HTTP messages.
+ * + * @throws IOException if writing fails + */ public void writeCRLF() throws IOException { write(new byte[]{'\r', '\n'}); } + /** + * Flushes buffered data to the underlying stream. + * + * @throws IOException if flushing fails + */ public void flush() throws IOException { flushInternal(); out.flush(); } + /** + * Closes the underlying output stream. + * + * @throws IOException if closing fails + */ public void close() throws IOException { out.close(); } + /** + * Writes buffered data to the underlying stream and + * resets the buffer position. + * + * @throws IOException if writing fails + */ private void flushInternal() throws IOException { if (position > 0) { out.write(buffer, 0, position);