diff --git a/src/main/java/io/github/lumijiez/core/http/HttpRequest.java b/src/main/java/io/github/lumijiez/core/http/HttpRequest.java index 325d436..18771eb 100644 --- a/src/main/java/io/github/lumijiez/core/http/HttpRequest.java +++ b/src/main/java/io/github/lumijiez/core/http/HttpRequest.java @@ -1,10 +1,15 @@ package io.github.lumijiez.core.http; import io.github.lumijiez.core.util.UrlParser; +import io.github.lumijiez.core.logging.Logger; import java.io.BufferedReader; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class HttpRequest { @@ -12,26 +17,38 @@ public class HttpRequest { private String path; private String httpVersion; private final Map headers; + private final Map> queryParams; private UrlParser urlParser; + private HttpRequestBody body; + private Map formData; + private final Map cookies; public HttpRequest(BufferedReader in) throws IOException { this.headers = new HashMap<>(); + this.queryParams = new HashMap<>(); + this.cookies = new HashMap<>(); parseRequest(in); + parseCookies(); + if (hasBody()) { + parseBody(in); + } } private void parseRequest(BufferedReader in) throws IOException { String requestLine = in.readLine(); - if (requestLine != null && !requestLine.trim().isEmpty()) { - String[] tokens = requestLine.split(" "); - if (tokens.length == 3) { - this.method = tokens[0]; - this.path = tokens[1]; - this.httpVersion = tokens[2]; - } else { - throw new IOException("Invalid request line format."); - } + if (requestLine == null || requestLine.trim().isEmpty()) { + throw new IOException("Empty request line"); } + String[] tokens = requestLine.split(" "); + if (tokens.length != 3) { + throw new IOException("Invalid request line format: " + requestLine); + } + + this.method = tokens[0].toUpperCase(); + parsePathAndQuery(tokens[1]); + this.httpVersion = tokens[2]; + String headerLine; while ((headerLine = in.readLine()) != null && !headerLine.trim().isEmpty()) { int separator = headerLine.indexOf(':'); @@ -39,37 +56,148 @@ public class HttpRequest { String key = headerLine.substring(0, separator).trim().toLowerCase(); String value = headerLine.substring(separator + 1).trim(); headers.put(key, value); + Logger.debug("HTTP", "Header: " + key + " = " + value); } } } - public boolean isKeepAlive() { - String connection = headers.get("connection"); - if ("close".equalsIgnoreCase(connection)) { + private void parsePathAndQuery(String fullPath) throws IOException { + int queryStart = fullPath.indexOf('?'); + if (queryStart != -1) { + this.path = fullPath.substring(0, queryStart); + parseQueryString(fullPath.substring(queryStart + 1)); + } else { + this.path = fullPath; + } + + this.path = URLDecoder.decode(this.path, StandardCharsets.UTF_8); + } + + private void parseQueryString(String queryString) throws IOException { + String[] pairs = queryString.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + if (idx > 0) { + String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8); + String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8); + + queryParams.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + Logger.debug("HTTP", "Query param: " + key + " = " + value); + } + } + } + + private void parseCookies() { + String cookieHeader = headers.get("cookie"); + if (cookieHeader != null) { + String[] cookiePairs = cookieHeader.split(";"); + for (String cookiePair : cookiePairs) { + String[] parts = cookiePair.trim().split("=", 2); + if (parts.length == 2) { + cookies.put(parts[0], parts[1]); + Logger.debug("HTTP", "Cookie: " + parts[0] + " = " + parts[1]); + } + } + } + } + + private boolean hasBody() { + if ("GET".equals(method) || "HEAD".equals(method)) { return false; } - return "HTTP/1.1".equals(httpVersion) || - "keep-alive".equalsIgnoreCase(connection); + + String contentLengthHeader = headers.get("content-length"); + return contentLengthHeader != null && + Integer.parseInt(contentLengthHeader) > 0; } - public void setUrlParser(UrlParser urlParser) { - this.urlParser = urlParser; + private void parseBody(BufferedReader in) throws IOException { + int contentLength = Integer.parseInt(headers.get("content-length")); + String contentType = headers.getOrDefault("content-type", "text/plain"); + + if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) { + parseChunkedBody(in); + return; + } + + char[] buffer = new char[contentLength]; + int totalRead = 0; + while (totalRead < contentLength) { + int read = in.read(buffer, totalRead, contentLength - totalRead); + if (read == -1) { + throw new IOException("Unexpected end of stream"); + } + totalRead += read; + } + String content = new String(buffer); + + if (contentType.startsWith("multipart/form-data")) { + HttpMultipartParser parser = new HttpMultipartParser(in, contentType); + this.formData = parser.parse(); + Logger.debug("HTTP", "Parsed multipart form data: " + formData.size() + " parts"); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + this.formData = parseUrlEncodedForm(content); + Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields"); + } else { + this.body = new HttpRequestBody(content, HttpContentType.fromString(contentType)); + Logger.debug("HTTP", "Parsed body with content type: " + contentType); + } } - public String getPathParam(String name) { - return urlParser != null ? urlParser.getPathParam(name) : null; + private void parseChunkedBody(BufferedReader in) throws IOException { + StringBuilder content = new StringBuilder(); + while (true) { + String chunkSizeLine = in.readLine(); + if (chunkSizeLine == null) { + throw new IOException("Unexpected end of stream in chunked body"); + } + + int chunkSize = Integer.parseInt(chunkSizeLine.trim(), 16); + if (chunkSize == 0) { + break; + } + + char[] chunk = new char[chunkSize]; + int totalRead = 0; + while (totalRead < chunkSize) { + int read = in.read(chunk, totalRead, chunkSize - totalRead); + if (read == -1) { + throw new IOException("Unexpected end of stream in chunk"); + } + totalRead += read; + } + content.append(chunk); + + in.readLine(); + } + + String line; + while ((line = in.readLine()) != null && !line.isEmpty()) { + // TO DO + // Trailing headers + // :/ + } + + String contentType = headers.getOrDefault("content-type", "text/plain"); + this.body = new HttpRequestBody(content.toString(), HttpContentType.fromString(contentType)); } - public String getQueryParam(String name) { - return urlParser != null ? urlParser.getQueryParam(name) : null; - } + private Map parseUrlEncodedForm(String content) { + Map params = new HashMap<>(); + if (content == null || content.trim().isEmpty()) { + return params; + } - public Map getPathParams() { - return urlParser != null ? urlParser.getPathParams() : Map.of(); - } - - public Map getQueryParams() { - return urlParser != null ? urlParser.getQueryParams() : Map.of(); + String[] pairs = content.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("=", 2); + if (keyValue.length == 2) { + String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8); + params.put(key, value); + } + } + return params; } public String getMethod() { @@ -83,4 +211,62 @@ public class HttpRequest { public String getHttpVersion() { return httpVersion; } -} + + public String getHeader(String name) { + return headers.get(name.toLowerCase()); + } + + public Map getHeaders() { + return new HashMap<>(headers); + } + + public HttpRequestBody getBody() { + return body; + } + + public Map getFormData() { + return formData != null ? new HashMap<>(formData) : Map.of(); + } + + public String getCookie(String name) { + return cookies.get(name); + } + + public Map getCookies() { + return new HashMap<>(cookies); + } + + public String getQueryParam(String name) { + List values = queryParams.get(name); + return values != null && !values.isEmpty() ? values.get(0) : null; + } + + public List getQueryParams(String name) { + return queryParams.getOrDefault(name, new ArrayList<>()); + } + + public Map> getAllQueryParams() { + return new HashMap<>(queryParams); + } + + public void setUrlParser(UrlParser urlParser) { + this.urlParser = urlParser; + } + + public String getPathParam(String name) { + return urlParser != null ? urlParser.getPathParam(name) : null; + } + + public Map getPathParams() { + return urlParser != null ? urlParser.getPathParams() : Map.of(); + } + + public boolean isKeepAlive() { + String connection = headers.get("connection"); + if ("close".equalsIgnoreCase(connection)) { + return false; + } + return "HTTP/1.1".equals(httpVersion) || + "keep-alive".equalsIgnoreCase(connection); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/lumijiez/core/http/HttpResponse.java b/src/main/java/io/github/lumijiez/core/http/HttpResponse.java index 11659dd..5f014d4 100644 --- a/src/main/java/io/github/lumijiez/core/http/HttpResponse.java +++ b/src/main/java/io/github/lumijiez/core/http/HttpResponse.java @@ -1,37 +1,66 @@ package io.github.lumijiez.core.http; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import io.github.lumijiez.core.logging.Logger; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; public class HttpResponse { private final BufferedWriter out; + private final Map headers; + private static final ObjectMapper jsonMapper = new ObjectMapper(); + private static final XmlMapper xmlMapper = new XmlMapper(); public HttpResponse(BufferedWriter out) { this.out = out; + this.headers = new HashMap<>(); + headers.put("Connection", "keep-alive"); + headers.put("Keep-Alive", "timeout=30"); + } + + public void setHeader(String name, String value) { + headers.put(name, value); } public void sendResponse(HttpStatus status, String message) throws IOException { + sendResponse(status, message, HttpContentType.TEXT_PLAIN); + } + + public void sendResponse(HttpStatus status, String message, HttpContentType contentType) throws IOException { Logger.info("HTTP", "Outgoing: " + status.getCode() + " " + message); - out.write("HTTP/1.1 " + status.getCode() + " " + status.getMessage()); + writeStatusLine(status); + writeHeaders(contentType, message.getBytes(StandardCharsets.UTF_8).length); out.write("\r\n"); - - out.write("Content-Type: text/plain"); - out.write("\r\n"); - out.write("Content-Length: " + message.getBytes(StandardCharsets.UTF_8).length); - out.write("\r\n"); - out.write("Connection: keep-alive"); - out.write("\r\n"); - out.write("Keep-Alive: timeout=30"); - out.write("\r\n"); - - out.write("\r\n"); - out.write(message); - out.flush(); } + + public void sendJson(HttpStatus status, Object obj) throws IOException { + String jsonContent = jsonMapper.writeValueAsString(obj); + sendResponse(status, jsonContent, HttpContentType.APPLICATION_JSON); + } + + public void sendXml(HttpStatus status, Object obj) throws IOException { + String xmlContent = xmlMapper.writeValueAsString(obj); + sendResponse(status, xmlContent, HttpContentType.APPLICATION_XML); + } + + private void writeStatusLine(HttpStatus status) throws IOException { + out.write("HTTP/1.1 " + status.getCode() + " " + status.getMessage() + "\r\n"); + } + + private void writeHeaders(HttpContentType contentType, int contentLength) throws IOException { + headers.put("Content-Type", contentType.getValue()); + headers.put("Content-Length", String.valueOf(contentLength)); + + for (Map.Entry header : headers.entrySet()) { + out.write(header.getKey() + ": " + header.getValue() + "\r\n"); + } + } } diff --git a/src/main/java/io/github/lumijiez/example/Main.java b/src/main/java/io/github/lumijiez/example/Main.java index ceb47b6..0629a47 100644 --- a/src/main/java/io/github/lumijiez/example/Main.java +++ b/src/main/java/io/github/lumijiez/example/Main.java @@ -32,6 +32,11 @@ public class Main { res.sendResponse(HttpStatus.OK, "All good, lil bro"); }); + server.GET("/user", (req, res) -> { + Product product = productDao.getProductById(5); + res.sendJson(HttpStatus.OK, product); + }); + server.GET("/products", (req, res) -> { Product product = productDao.getProductById(5); res.sendResponse(HttpStatus.OK, product.toString());