diff --git a/study/build.gradle b/study/build.gradle index 87a1f0313c..b8f82f0e57 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -21,7 +21,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' - implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' + implementation 'com.github.jknack:handlebars-springmvc:4.4.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.26.0' diff --git a/study/src/main/java/cache/com/example/version/HandlebarsConfig.java b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java new file mode 100644 index 0000000000..e509013696 --- /dev/null +++ b/study/src/main/java/cache/com/example/version/HandlebarsConfig.java @@ -0,0 +1,26 @@ +package cache.com.example.version; + +import com.github.jknack.handlebars.springmvc.HandlebarsViewResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.ViewResolver; + +@Configuration +public class HandlebarsConfig { + + private final VersionHandlebarsHelper versionHandlebarsHelper; + + public HandlebarsConfig(VersionHandlebarsHelper versionHandlebarsHelper) { + this.versionHandlebarsHelper = versionHandlebarsHelper; + } + + @Bean + public ViewResolver handlebarsViewResolver() { + HandlebarsViewResolver viewResolver = new HandlebarsViewResolver(); + viewResolver.registerHelper("staticUrls", versionHandlebarsHelper); + viewResolver.setPrefix("classpath:/templates/"); + viewResolver.setSuffix(".html"); + + return viewResolver; + } +} diff --git a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java index a8e004466a..8bf617189f 100644 --- a/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java +++ b/study/src/main/java/cache/com/example/version/VersionHandlebarsHelper.java @@ -1,13 +1,14 @@ package cache.com.example.version; +import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import pl.allegro.tech.boot.autoconfigure.handlebars.HandlebarsHelper; +import org.springframework.stereotype.Component; -@HandlebarsHelper -public class VersionHandlebarsHelper { +@Component +public class VersionHandlebarsHelper implements Helper { private static final Logger log = LoggerFactory.getLogger(VersionHandlebarsHelper.class); @@ -22,4 +23,9 @@ public String staticUrls(String path, Options options) { log.debug("static url : {}", path); return String.format("/resources/%s%s", version.getVersion(), path); } + + @Override + public Object apply(Object context, Options options) { + return staticUrls(context.toString(), options); + } } diff --git a/tomcat/src/main/java/com/techcourse/controller/ControllerRegistry.java b/tomcat/src/main/java/com/techcourse/controller/ControllerRegistry.java new file mode 100644 index 0000000000..1a8b17d4ee --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/ControllerRegistry.java @@ -0,0 +1,12 @@ +package com.techcourse.controller; + +import org.apache.catalina.Controller; +import java.util.Map; + +public class ControllerRegistry { + public static void registerControllers(Map controllers) { + controllers.put("/", new HomeController()); + controllers.put("/login", new LoginController()); + controllers.put("/register", new RegisterController()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/HomeController.java b/tomcat/src/main/java/com/techcourse/controller/HomeController.java new file mode 100644 index 0000000000..c8b5948824 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/HomeController.java @@ -0,0 +1,17 @@ +package com.techcourse.controller; + +import org.apache.catalina.AbstractController; +import org.apache.catalina.Controller; +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.response.ContentType; +import org.apache.coyote.http11.response.Http11Response; +import org.apache.coyote.http11.response.Http11ResponseBuilder; +import org.apache.coyote.http11.response.StatusCode; + +public class HomeController extends AbstractController { + + @Override + protected void doGet(Http11Request request, Http11Response response) throws Exception { + Http11ResponseBuilder.build(response, StatusCode.OK, ContentType.TEXT_HTML_UTF8, "Hello world!"); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java new file mode 100644 index 0000000000..16ec886649 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,60 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.catalina.AbstractController; +import org.apache.coyote.http11.cookie.HttpCookie; +import org.apache.coyote.http11.cookie.Session; +import org.apache.coyote.http11.cookie.SessionManager; +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.response.ContentType; +import org.apache.coyote.http11.response.Http11Response; +import org.apache.coyote.http11.response.Http11ResponseBuilder; +import org.apache.coyote.http11.response.StatusCode; +import java.util.Optional; +import java.util.UUID; + +public class LoginController extends AbstractController { + + private final SessionManager sessionManager = new SessionManager(); + + @Override + protected void doGet(Http11Request request, Http11Response response) throws Exception { + String sessionId = request.getSessionCookie(); + if (sessionId != null && sessionManager.findSession(sessionId) != null) { + Http11ResponseBuilder.buildRedirect(response, request.getCookie(), "index.html"); + return; + } + Http11ResponseBuilder.buildFile(response, StatusCode.OK, ContentType.TEXT_HTML_UTF8, "login.html"); + } + + @Override + protected void doPost(Http11Request request, Http11Response response) throws Exception { + String sessionId = request.getSessionCookie(); + if (sessionId != null && sessionManager.findSession(sessionId) != null) { + Http11ResponseBuilder.buildRedirect(response, request.getCookie(), "index.html"); + return; + } + + String account = request.getBodyValue("account"); + String password = request.getBodyValue("password"); + + if (account == null || password == null) { + Http11ResponseBuilder.buildFile(response, StatusCode.OK, ContentType.TEXT_HTML_UTF8, "login.html"); + return; + } + + Optional user = InMemoryUserRepository.findByAccount(account); + if (user.isPresent() && user.get().checkPassword(password)) { + sessionId = UUID.randomUUID().toString(); + sessionManager.add(new Session(sessionId)); + HttpCookie httpCookie = new HttpCookie(); + httpCookie.putSessionCookie(sessionId); + Http11ResponseBuilder.buildRedirect(response, request.getCookie(), "index.html"); + return; + } + + Http11ResponseBuilder.buildFile(response, StatusCode.UNAUTHORIZED, ContentType.TEXT_HTML_UTF8, "401.html"); + } +} + diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java new file mode 100644 index 0000000000..cce6c34452 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,36 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.catalina.AbstractController; +import org.apache.catalina.Controller; +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.response.ContentType; +import org.apache.coyote.http11.response.Http11Response; +import org.apache.coyote.http11.response.Http11ResponseBuilder; +import org.apache.coyote.http11.response.StatusCode; + +public class RegisterController extends AbstractController { + + @Override + protected void doGet(Http11Request request, Http11Response response) throws Exception { + Http11ResponseBuilder.buildFile(response, StatusCode.OK, ContentType.TEXT_HTML_UTF8, "register.html"); + } + + @Override + protected void doPost(Http11Request request, Http11Response response) throws Exception { + String account = request.getBodyValue("account"); + String email = request.getBodyValue("email"); + String password = request.getBodyValue("password"); + + if (account == null || email == null || password == null) { + Http11ResponseBuilder.buildFile(response, StatusCode.OK, ContentType.TEXT_HTML_UTF8, "register.html"); + return; + } + + User user = new User(account, email, password); + InMemoryUserRepository.save(user); + Http11ResponseBuilder.buildRedirect(response, null, "index.html"); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/AbstractController.java b/tomcat/src/main/java/org/apache/catalina/AbstractController.java new file mode 100644 index 0000000000..5abdc615d7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/AbstractController.java @@ -0,0 +1,26 @@ +package org.apache.catalina; + +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.response.Http11Response; + +public abstract class AbstractController implements Controller { + + @Override + public void service(Http11Request request, Http11Response response) throws Exception { + HttpMethod method = request.getHttpMethod(); + if (method.equals(HttpMethod.GET)) { + doGet(request, response); + } else if (method.equals(HttpMethod.POST)) { + doPost(request, response); + } + } + + protected void doGet(Http11Request request, Http11Response response) throws Exception { + throw new UnsupportedOperationException("GET method not implemented"); + } + + protected void doPost(Http11Request request, Http11Response response) throws Exception { + throw new UnsupportedOperationException("POST method not implemented"); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Controller.java b/tomcat/src/main/java/org/apache/catalina/Controller.java new file mode 100644 index 0000000000..3ce9559542 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Controller.java @@ -0,0 +1,8 @@ +package org.apache.catalina; + +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.response.Http11Response; + +public interface Controller { + void service(Http11Request request, Http11Response response) throws Exception; +} diff --git a/tomcat/src/main/java/org/apache/catalina/FileController.java b/tomcat/src/main/java/org/apache/catalina/FileController.java new file mode 100644 index 0000000000..d8c7419514 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/FileController.java @@ -0,0 +1,19 @@ +package org.apache.catalina; + +import org.apache.coyote.http11.request.Http11Request; +import org.apache.coyote.http11.response.ContentType; +import org.apache.coyote.http11.response.Http11Response; +import org.apache.coyote.http11.response.Http11ResponseBuilder; +import org.apache.coyote.http11.response.StatusCode; + +public class FileController implements Controller { + + @Override + public void service(Http11Request request, Http11Response response) throws Exception { + String fileName = request.getUri(); + int dotIndex = fileName.lastIndexOf('.'); + String extension = fileName.substring(dotIndex + 1); + + Http11ResponseBuilder.buildFile(response, StatusCode.OK, ContentType.fromExtension(extension), fileName); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..f8cbea6074 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -8,6 +8,8 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class Connector implements Runnable { @@ -15,23 +17,27 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int MAX_THREADS = 200; + private static final int ACCEPT_COUNT = 25; private final ServerSocket serverSocket; + private final ExecutorService executor; private boolean stopped; public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, MAX_THREADS); } - public Connector(final int port, final int acceptCount) { - this.serverSocket = createServerSocket(port, acceptCount); + public Connector(final int port, final int acceptCount, final int maxThreads) { + this.serverSocket = createServerSocket(port); this.stopped = false; + this.executor = Executors.newFixedThreadPool(MAX_THREADS); } - private ServerSocket createServerSocket(final int port, final int acceptCount) { + private ServerSocket createServerSocket(final int port) { try { final int checkedPort = checkPort(port); - final int checkedAcceptCount = checkAcceptCount(acceptCount); + final int checkedAcceptCount = checkAcceptCount(ACCEPT_COUNT); return new ServerSocket(checkedPort, checkedAcceptCount); } catch (IOException e) { throw new UncheckedIOException(e); @@ -67,7 +73,9 @@ private void process(final Socket connection) { return; } var processor = new Http11Processor(connection); - new Thread(processor).start(); + executor.submit(() -> { + new Thread(processor).start(); + }); } public void stop() { @@ -76,6 +84,8 @@ public void stop() { serverSocket.close(); } catch (IOException e) { log.error(e.getMessage(), e); + } finally { + executor.shutdown(); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 22cf65a19b..9e050bfdf8 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,30 +1,22 @@ package org.apache.coyote.http11; -import com.techcourse.db.InMemoryUserRepository; -import com.techcourse.exception.UncheckedServletException; -import com.techcourse.model.User; +import org.apache.catalina.Controller; +import org.apache.catalina.FileController; import org.apache.coyote.Processor; import org.apache.coyote.http11.request.Http11Request; import org.apache.coyote.http11.request.Http11RequestBuilder; -import org.apache.coyote.http11.response.ContentType; import org.apache.coyote.http11.response.Http11Response; -import org.apache.coyote.http11.response.Http11ResponseBuilder; import org.apache.coyote.http11.response.Http11ResponseWriter; -import org.apache.coyote.http11.response.StatusCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; import java.net.Socket; -import java.net.URL; -import java.nio.file.Files; -import java.util.Optional; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; + private final RequestMapping requestMapping = new RequestMapping(); public Http11Processor(final Socket connection) { this.connection = connection; @@ -42,44 +34,23 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream()) { Http11Request request = Http11RequestBuilder.build(inputStream); - Http11Response response = catalina(request); + Http11Response response = handleRequest(request); outputStream.write(Http11ResponseWriter.write(response)); outputStream.flush(); - } catch (IOException | UncheckedServletException e) { + } catch (Exception e) { log.error(e.getMessage(), e); } } - public Http11Response catalina(Http11Request request) throws IOException { - if (request.getUri().equals("/")) { - return Http11ResponseBuilder.build(StatusCode.OK, ContentType.TEXT_HTML_UTF8, "Hello world!"); + private Http11Response handleRequest(Http11Request request) throws Exception { + Controller controller = requestMapping.getController(request); + if (controller == null) { + controller = new FileController(); } - - if (request.getUri().equals("/login")) { - final URL resource = getClass().getClassLoader().getResource("static/login.html"); - String filePath = resource.getFile(); - final String responseBody = new String(Files.readAllBytes(new File(filePath).toPath())); - String account = request.getQueryParameter("account"); - String password = request.getQueryParameter("password"); - Optional user = InMemoryUserRepository.findByAccount(account); - if (user.isPresent()) { - User currentUser = user.get(); - if (currentUser.checkPassword(password)) { - System.out.println(currentUser); - } - } - return Http11ResponseBuilder.build(StatusCode.OK, ContentType.TEXT_HTML_UTF8, responseBody); - } - - final URL resource = getClass().getClassLoader().getResource("static/" + request.getUri()); - String filePath = resource.getFile(); - int dotIndex = filePath.lastIndexOf('.'); - String extension = filePath.substring(dotIndex + 1); - - final String responseBody = new String(Files.readAllBytes(new File(filePath).toPath())); - - return Http11ResponseBuilder.build(StatusCode.OK, ContentType.fromExtension(extension), responseBody); + Http11Response response = new Http11Response(); + controller.service(request, response); + return response; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java new file mode 100644 index 0000000000..4043775902 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java @@ -0,0 +1,20 @@ +package org.apache.coyote.http11; + +import com.techcourse.controller.ControllerRegistry; +import org.apache.catalina.Controller; +import org.apache.coyote.http11.request.Http11Request; +import java.util.HashMap; +import java.util.Map; + +public class RequestMapping { + + private final Map controllers = new HashMap<>(); + + public RequestMapping() { + ControllerRegistry.registerControllers(controllers); + } + + public Controller getController(Http11Request request) { + return controllers.get(request.getUri()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java new file mode 100644 index 0000000000..0e040c13b8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.cookie; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class HttpCookie { + + private static final String SESSION_ID = "JSESSIONID"; + + private final Map cookies; + + public HttpCookie() { + this.cookies = new HashMap<>(); + } + + public void putSessionCookie(String value) { + if (!cookies.containsKey(SESSION_ID)) { + put(SESSION_ID, value); + } + } + + public String getSessionCookie() { + return cookies.get(SESSION_ID); + } + + public void put(String name, String value) { + cookies.put(name, value); + } + + public String get(String name) { + return cookies.get(name); + } + + public Map getCookies() { + return cookies; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/cookie/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/cookie/Session.java new file mode 100644 index 0000000000..ea864e51fd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/cookie/Session.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11.cookie; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(final String name) { + return values.get(name); + } + + public void setAttribute(final String name, final Object value) { + values.put(name, value); + } + + public void removeAttribute(final String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/cookie/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/cookie/SessionManager.java new file mode 100644 index 0000000000..35587156eb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/cookie/SessionManager.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.cookie; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager { + + private static final Map SESSIONS = new ConcurrentHashMap<>(); + + public SessionManager() {} + + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } + + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + public void remove(Session session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Http11Request.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Http11Request.java index 70ae7469e3..c2ab9bd390 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Http11Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Http11Request.java @@ -1,13 +1,16 @@ package org.apache.coyote.http11.request; +import org.apache.coyote.http11.cookie.HttpCookie; +import java.util.Map; + public class Http11Request { private final RequestLine requestLine; private final RequestHeader header; - private final String body; + private final Map body; - public Http11Request(RequestLine requestLine, RequestHeader header, String body) { + public Http11Request(RequestLine requestLine, RequestHeader header, Map body) { this.requestLine = requestLine; this.header = header; this.body = body; @@ -41,7 +44,19 @@ public Object getHeaderValue(String name) { return header.get(name); } - public String getBody() { + public HttpCookie getCookie() { + return header.getCookie(); + } + + public String getSessionCookie() { + return header.getSessionCookie(); + } + + public String getBodyValue(String name) { + return body.get(name); + } + + public Map getBody() { return body; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Http11RequestBuilder.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Http11RequestBuilder.java index 7d5128ed55..72c555b7f5 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Http11RequestBuilder.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Http11RequestBuilder.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -12,6 +13,7 @@ public class Http11RequestBuilder { private static final String HTTP_11 = "HTTP/1.1"; private static final String CONTENT_LENGTH = "Content-Length"; private static final String HEADER_REGEX = ": "; + private static final String COOKIE_HEADER = "Cookie"; private static final int HEADER_PART_LENGTH = 2; private static final int HEADER_KEY_INDEX = 0; private static final int HEADER_VALUE_INDEX = 1; @@ -20,7 +22,7 @@ public static Http11Request build(InputStream inputStream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); RequestLine requestLine = buildRequestLine(reader); RequestHeader requestHeader = buildRequestHeader(reader); - String body = buildRequestBody(requestHeader, reader); + Map body = buildRequestBody(requestHeader, reader); return new Http11Request(requestLine, requestHeader, body); } @@ -76,18 +78,33 @@ private static RequestHeader buildRequestHeader(BufferedReader reader) throws IO RequestHeader requestHeader = new RequestHeader(); while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) { String[] headerParts = headerLine.split(HEADER_REGEX); - putHeader(headerParts, requestHeader); + buildCookieOrOtherHeader(headerParts, requestHeader); } return requestHeader; } + private static void buildCookieOrOtherHeader(String[] headerParts, RequestHeader requestHeader) { + if (headerParts[HEADER_KEY_INDEX].equals(COOKIE_HEADER)) { + buildCookie(headerParts[HEADER_VALUE_INDEX], requestHeader); + return; + } + putHeader(headerParts, requestHeader); + } + + private static void buildCookie(String cookies, RequestHeader requestHeader) { + Arrays.stream(cookies.split("; ")) + .map(pair -> pair.split("=", HEADER_PART_LENGTH)) + .filter(keyValue -> keyValue.length == 2) + .forEach(keyValue -> requestHeader.putCookie(keyValue[0], keyValue[1])); + } + private static void putHeader(String[] headerParts, RequestHeader requestHeader) { if (headerParts.length == HEADER_PART_LENGTH) { requestHeader.put(headerParts[HEADER_KEY_INDEX], headerParts[HEADER_VALUE_INDEX]); } } - private static String buildRequestBody(RequestHeader requestHeader, BufferedReader reader) throws IOException { + private static Map buildRequestBody(RequestHeader requestHeader, BufferedReader reader) throws IOException { String contentLengthHeader = (String) requestHeader.get(CONTENT_LENGTH); String body = null; if (contentLengthHeader != null) { @@ -96,7 +113,42 @@ private static String buildRequestBody(RequestHeader requestHeader, BufferedRead reader.read(bodyChars); body = new String(bodyChars); } - return body; + return parsingRequestBody(body); + } + + private static Map parsingRequestBody(String body) { + Map result = new HashMap<>(); + if (body == null || body.isEmpty()) { + return result; + } + + if (!body.contains("=") && !body.contains("&")) { + result.put("body", body); + return result; + } + + String[] pairs = body.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("=", 2); // 최대 2개로만 split + parsingKeyAndValue(keyValue, result); + parsingKey(keyValue, result); + } + return result; + } + + private static void parsingKeyAndValue(String[] keyValue, Map result) { + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + result.put(key, value); + } + } + + private static void parsingKey(String[] keyValue, Map result) { + if (keyValue.length == 1) { + String key = keyValue[0]; + result.put(key, ""); + } } private static void validateRequestLine(String request) throws IOException { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java index 5c154c89c0..e74788ace1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -3,7 +3,8 @@ import java.util.Arrays; public enum HttpMethod { - GET; + GET, + POST; public static HttpMethod from(String method) { return Arrays.stream(values()) diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java index b2b1e9aa79..139bc01148 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java @@ -1,11 +1,18 @@ package org.apache.coyote.http11.request; +import org.apache.coyote.http11.cookie.HttpCookie; import java.util.HashMap; import java.util.Map; public class RequestHeader { - private final Map headers = new HashMap<>(); + private final Map headers; + private final HttpCookie cookie; + + public RequestHeader() { + this.headers = new HashMap<>(); + this.cookie = new HttpCookie(); + } public void put(String name, Object value) { headers.put(name, value); @@ -14,4 +21,20 @@ public void put(String name, Object value) { public Object get(String name) { return headers.get(name); } + + public void putCookie(String name, String value) { + cookie.put(name, value); + } + + public String getCookieValue(String name) { + return cookie.get(name); + } + + public String getSessionCookie() { + return cookie.getSessionCookie(); + } + + public HttpCookie getCookie() { + return cookie; + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11Response.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11Response.java index 9a7dd460e9..a493429687 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11Response.java @@ -2,11 +2,13 @@ public class Http11Response { - private final ResponseStatusLine statusLine; - private final ResponseHeader header; - private final String body; + private ResponseStatusLine statusLine; + private ResponseHeader header; + private String body; - public Http11Response(ResponseStatusLine statusLine, ResponseHeader header, String body) { + public Http11Response() {} + + public void add(ResponseStatusLine statusLine, ResponseHeader header, String body) { this.statusLine = statusLine; this.header = header; this.body = body; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseBuilder.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseBuilder.java index 307b504487..df74e3543c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseBuilder.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseBuilder.java @@ -1,12 +1,47 @@ package org.apache.coyote.http11.response; +import org.apache.coyote.http11.cookie.HttpCookie; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + public class Http11ResponseBuilder { private static final String HTTP_11 = "HTTP/1.1"; - public static Http11Response build(StatusCode statusCode, ContentType contentType, String body) { + public static void build(Http11Response response, StatusCode statusCode, ContentType contentType, HttpCookie cookie, String body) { ResponseStatusLine statusLine = new ResponseStatusLine(HTTP_11, statusCode); - ResponseHeader header = new ResponseHeader(contentType, body.getBytes().length); - return new Http11Response(statusLine, header, body); + ResponseHeader header = new ResponseHeader(contentType, (long) body.getBytes().length, cookie); + response.add(statusLine, header, body); + } + + public static void build(Http11Response response, StatusCode statusCode, ContentType contentType, String body) { + build(response, statusCode, contentType, null, body); + } + + public static void buildFile(Http11Response response, StatusCode statusCode, + ContentType contentType, HttpCookie cookie, String filename) throws IOException { + URL resource = Http11ResponseBuilder.class.getClassLoader().getResource("static/" + filename); + + if (resource == null) { + throw new FileNotFoundException("Resource not found: " + filename); + } + + String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + build(response, statusCode, contentType, cookie, responseBody); + } + + public static void buildFile(Http11Response response, StatusCode statusCode, + ContentType contentType, String filename) throws IOException { + buildFile(response, statusCode, contentType, null, filename); + } + + public static void buildRedirect(Http11Response response, HttpCookie cookie, String redirectUri) { + ResponseStatusLine statusLine = new ResponseStatusLine(HTTP_11, StatusCode.FOUND); + ResponseHeader header = new ResponseHeader(null, null, cookie); + header.addLocation(redirectUri); + response.add(statusLine, header, null); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseWriter.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseWriter.java index 05db2ad38d..cd1096e1d9 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseWriter.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Http11ResponseWriter.java @@ -1,11 +1,15 @@ package org.apache.coyote.http11.response; +import jakarta.servlet.http.Cookie; +import org.apache.coyote.http11.cookie.HttpCookie; import java.nio.charset.StandardCharsets; public class Http11ResponseWriter { private final static String CONTENT_TYPE = "Content-Type"; private final static String CONTENT_LENGTH = "Content-Length"; + private final static String COOKIE = "Set-Cookie: "; + private final static String LOCATION = "Location"; public static byte[] write(Http11Response response) { String responseString = String.format("%s%s%n%s", @@ -25,12 +29,18 @@ private static String writeStatusLine(ResponseStatusLine statusLine) { } private static String writeHeader(ResponseHeader header) { - return String.format("%s%s", + return String.format("%s%s%s%s", + writeCookie(header.getCookie()), + writeLocation(header.getLocation()), writeContentType(header.getContentType()), writeContentLength(header.getContentLength())); } private static String writeContentType(ContentType contentType) { + if (contentType == null) { + return ""; + } + String mediaType = contentType.getMediaType(); String parameter = contentType.getParameter(); @@ -41,7 +51,32 @@ private static String writeContentType(ContentType contentType) { return String.format("%s: %s %n", CONTENT_TYPE, mediaType); } - private static String writeContentLength(long contentLength) { + private static String writeContentLength(Long contentLength) { + if (contentLength == null) { + return ""; + } return String.format("%s: %s %n", CONTENT_LENGTH, contentLength); } + + private static String writeCookie(HttpCookie cookie) { + if (cookie == null || cookie.getCookies().isEmpty()) { + return ""; + } + StringBuilder cookieResponse = new StringBuilder(COOKIE); + cookie.getCookies().forEach((key, value) -> + cookieResponse.append(key).append("=").append(value).append("; ") + ); + + if (cookieResponse.length() > COOKIE.length()) { + cookieResponse.setLength(cookieResponse.length() - 2); + } + return String.format("%s%n", cookieResponse); + } + + private static String writeLocation(String location) { + if (location == null) { + return ""; + } + return String.format("%s: %s%n", LOCATION, location); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java index ad7bbdc015..dd612fe6ee 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/ResponseHeader.java @@ -1,20 +1,38 @@ package org.apache.coyote.http11.response; +import jakarta.servlet.http.Cookie; +import org.apache.coyote.http11.cookie.HttpCookie; + public class ResponseHeader { private final ContentType contentType; - private final long contentLength; + private final Long contentLength; + private final HttpCookie cookie; + private String location; - public ResponseHeader(ContentType contentType, long contentLength) { + public ResponseHeader(ContentType contentType, Long contentLength, HttpCookie cookie) { this.contentType = contentType; this.contentLength = contentLength; + this.cookie = cookie; + } + + public void addLocation(String location) { + this.location = location; } public ContentType getContentType() { return contentType; } - public long getContentLength() { + public Long getContentLength() { return contentLength; } + + public HttpCookie getCookie() { + return cookie; + } + + public String getLocation() { + return location; + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java index 9d3df6e7d9..68cfa5665d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java @@ -2,7 +2,9 @@ public enum StatusCode { - OK(200, "OK") + OK(200, "OK"), + FOUND(302, "FOUND"), + UNAUTHORIZED(401, "UNAUTHORIZED") ; private final int code; diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
diff --git a/tomcat/src/test/java/org/apache/coyote/http11/cookie/HttpCookieTest.java b/tomcat/src/test/java/org/apache/coyote/http11/cookie/HttpCookieTest.java new file mode 100644 index 0000000000..b339e37303 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/cookie/HttpCookieTest.java @@ -0,0 +1,88 @@ +package org.apache.coyote.http11.cookie; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class HttpCookieTest { + + @DisplayName("쿠키 추가 성공") + @Test + void putSessionCookie_addsSessionCookie() { + // given + HttpCookie httpCookie = new HttpCookie(); + + // when + httpCookie.putSessionCookie("cookie"); + + // then + Map cookies = httpCookie.getCookies(); + assertAll( + () -> assertThat(cookies).containsKey("JSESSIONID"), + () -> assertThat(cookies.get("JSESSIONID")).isNotBlank() + ); + } + + @DisplayName("쿠키 추가 성공 : 중복 추가 방지") + @Test + void putSessionCookie_notDuplication() { + // given + HttpCookie httpCookie = new HttpCookie(); + httpCookie.put("JSESSIONID", "existing-session-id"); + + // when + httpCookie.putSessionCookie("cookie"); + + // then + Map cookies = httpCookie.getCookies(); + assertAll( + () -> assertThat(cookies).containsKey("JSESSIONID"), + () -> assertThat(cookies.get("JSESSIONID")).isEqualTo("existing-session-id") + ); + } + + @DisplayName("쿠키 추가 성공") + @Test + void put() { + // given + HttpCookie httpCookie = new HttpCookie(); + + // when + httpCookie.put("testCookie", "testValue"); + + // then + Map cookies = httpCookie.getCookies(); + assertThat(cookies).containsEntry("testCookie", "testValue"); + } + + @DisplayName("쿠키 조회 성공") + @Test + void get() { + // given + HttpCookie httpCookie = new HttpCookie(); + httpCookie.put("testCookie", "testValue"); + + // when + String value = httpCookie.get("testCookie"); + + // then + assertThat(value).isEqualTo("testValue"); + } + + @DisplayName("쿠키 조회 성공 : 존재하지 않는 쿠키 조회 시 null 반환") + @Test + void get_nonExistCookie() { + // given + HttpCookie httpCookie = new HttpCookie(); + + // when + String value = httpCookie.get("nonExistentCookie"); + + // then + assertThat(value).isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/Http11RequestBuilderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/Http11RequestBuilderTest.java index 40b1d2af35..4d587e4678 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/Http11RequestBuilderTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/Http11RequestBuilderTest.java @@ -30,7 +30,7 @@ void build() throws Exception { () -> assertThat(request.getQueryParameter("account")).isEqualTo("gugu"), () -> assertThat(request.getQueryParameter("password")).isEqualTo("password"), () -> assertThat(request.getHeaderValue("Content-Length")).isEqualTo("11"), - () -> assertThat(request.getBody()).isEqualTo("Hello World") + () -> assertThat(request.getBodyValue("body")).isEqualTo("Hello World") ); } @@ -66,7 +66,7 @@ void build_noBody_success() throws Exception { () -> assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET), () -> assertThat(request.getUri()).isEqualTo("/login"), () -> assertThat(request.getQueryParameter("account")).isEqualTo("gugu"), - () -> assertThat(request.getBody()).isNull() + () -> assertThat(request.getBody()).isEmpty() ); } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseBuilderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseBuilderTest.java index 1657949f44..b66bd03537 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseBuilderTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseBuilderTest.java @@ -15,7 +15,8 @@ void build() { ContentType contentType = ContentType.TEXT_HTML_UTF8; String body = "Hello World"; - Http11Response response = Http11ResponseBuilder.build(statusCode, contentType, body); + Http11Response response = new Http11Response(); + Http11ResponseBuilder.build(response, statusCode, contentType, body); assertAll( () -> assertThat(response.getHttpVersion()).isEqualTo("HTTP/1.1"), @@ -33,7 +34,8 @@ void build_emptyBody() { ContentType contentType = ContentType.TEXT_HTML_UTF8; String body = ""; - Http11Response response = Http11ResponseBuilder.build(statusCode, contentType, body); + Http11Response response = new Http11Response(); + Http11ResponseBuilder.build(response, statusCode, contentType, body); assertAll( () -> assertThat(response.getHttpVersion()).isEqualTo("HTTP/1.1"), diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseWriterTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseWriterTest.java index e8d5064ee6..1d1b2a017f 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseWriterTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/Http11ResponseWriterTest.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11.response; +import org.apache.coyote.http11.cookie.HttpCookie; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,8 +19,9 @@ void write() { String body = "Hello World"; ResponseStatusLine statusLine = new ResponseStatusLine("HTTP/1.1", statusCode); - ResponseHeader header = new ResponseHeader(contentType, body.getBytes(StandardCharsets.UTF_8).length); - Http11Response response = new Http11Response(statusLine, header, body); + ResponseHeader header = new ResponseHeader(contentType, (long) body.getBytes(StandardCharsets.UTF_8).length, new HttpCookie()); + Http11Response response = new Http11Response(); + response.add(statusLine, header, body); byte[] result = Http11ResponseWriter.write(response); String resultString = new String(result, StandardCharsets.UTF_8);