diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..b463c2b984 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -41,7 +41,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..28b6e8f11c 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -28,11 +28,14 @@ void testNewFixedThreadPool() { final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); executor.submit(logWithSleep("hello fixed thread pools")); executor.submit(logWithSleep("hello fixed thread pools")); + //() -> {} 로 바꾸면 queueSize 가 0으로 줄어듬 executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + //-> 스레드 개수를 2개로 지정해 두었으니 expectedPoolSize 는 2개 + final int expectedQueueSize = 1; + //-> 아직 2개의 스레드가 모두 종료되지 않았기 때문에 대기중인 스레드 Queue 의 size() 값은 1 assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -41,16 +44,27 @@ void testNewFixedThreadPool() { @Test void testNewCachedThreadPool() { final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); + //이 테스트는 뭐지?? + //https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newCachedThreadPool-- executor.submit(logWithSleep("hello cached thread pools")); executor.submit(logWithSleep("hello cached thread pools")); executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); + /** + * 정리하면 + * newFixedThreadPool : 최대 스레드 크기를 지정 + * 매번 새로운 스레드 생성? + * + * Cached~ + * : 가용 시스템 리소스가 없을 때 까지 생성 가능 + * : 재활용할 수 있음 + */ } private Runnable logWithSleep(final String message) { diff --git a/study/src/test/java/thread/stage0/ThreadTest.java b/study/src/test/java/thread/stage0/ThreadTest.java index 3ffef18869..c145ad4a55 100644 --- a/study/src/test/java/thread/stage0/ThreadTest.java +++ b/study/src/test/java/thread/stage0/ThreadTest.java @@ -1,5 +1,9 @@ package thread.stage0; +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.Thread.State; + import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,10 +12,10 @@ * 자바로 동시에 여러 작업을 처리할 때 스레드를 사용한다. * 스레드 객체를 직접 생성하는 방법부터 알아보자. * 진행하면서 막히는 부분은 아래 링크를 참고해서 해결한다. - * + *

* Thread Objects * https://docs.oracle.com/javase/tutorial/essential/concurrency/threads.html - * + *

* Defining and Starting a Thread * https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html */ @@ -28,12 +32,15 @@ class ThreadTest { void testExtendedThread() throws InterruptedException { // 하단의 ExtendedThread 클래스를 Thread 클래스로 상속하고 스레드 객체를 생성한다. Thread thread = new ExtendedThread("hello thread"); + assertThat(thread.getState()).isEqualTo(State.NEW); // 생성한 thread 객체를 시작한다. - thread.start(); + thread.start(); + assertThat(thread.getState()).isEqualTo(State.RUNNABLE); // thread의 작업이 완료될 때까지 기다린다. - thread.join(); + thread.join(); + assertThat(thread.getState()).isEqualTo(State.TERMINATED); } /** @@ -46,10 +53,10 @@ void testRunnableThread() throws InterruptedException { Thread thread = new Thread(new RunnableThread("hello thread")); // 생성한 thread 객체를 시작한다. - thread.start(); + thread.start(); // thread의 작업이 완료될 때까지 기다린다. - thread.join(); + thread.join(); } private static final class ExtendedThread extends Thread { diff --git a/study/src/test/java/thread/stage1/UserServlet.java b/study/src/test/java/thread/stage1/UserServlet.java index b180a84c32..a78e7b4ad4 100644 --- a/study/src/test/java/thread/stage1/UserServlet.java +++ b/study/src/test/java/thread/stage1/UserServlet.java @@ -11,7 +11,7 @@ public void service(final User user) { join(user); } - private void join(final User user) { + private synchronized void join(final User user) { if (!users.contains(user)) { users.add(user); } diff --git a/tomcat/build.gradle b/tomcat/build.gradle index ba598c228d..c5f1fbb5ca 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -25,6 +25,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.12.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' } test { diff --git a/tomcat/src/main/java/com/techcourse/Application.java b/tomcat/src/main/java/com/techcourse/Application.java index f874a4efb5..c4b9e86137 100644 --- a/tomcat/src/main/java/com/techcourse/Application.java +++ b/tomcat/src/main/java/com/techcourse/Application.java @@ -1,11 +1,17 @@ package com.techcourse; +import org.apache.catalina.servlet.PathMatchServletContainer; +import org.apache.catalina.servlet.RequestMapping; import org.apache.catalina.startup.Tomcat; +import com.techcourse.config.RequestMappingConfig; + public class Application { public static void main(String[] args) { - var tomcat = new Tomcat(); + RequestMapping requestMapping = RequestMappingConfig.getRequestMapping(); + PathMatchServletContainer servletContainer = new PathMatchServletContainer(requestMapping); + var tomcat = new Tomcat(servletContainer); tomcat.start(); } } diff --git a/tomcat/src/main/java/com/techcourse/config/RequestMappingConfig.java b/tomcat/src/main/java/com/techcourse/config/RequestMappingConfig.java new file mode 100644 index 0000000000..50649c1eb9 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/config/RequestMappingConfig.java @@ -0,0 +1,23 @@ +package com.techcourse.config; + +import java.util.Map; + +import org.apache.catalina.servlet.RequestMapping; + +import com.techcourse.controller.LoginController; +import com.techcourse.controller.RegisterController; +import com.techcourse.controller.StaticResourceController; + +public class RequestMappingConfig { + public static RequestMapping getRequestMapping() { + var staticResourceController = new StaticResourceController(); + return new RequestMapping( + Map.of( + "/login", new LoginController(), + "/register", new RegisterController(), + "/", staticResourceController, + ".*\\.(js|html|css)$", staticResourceController + ) + ); + } +} 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..c2b8933b2b --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,50 @@ +package com.techcourse.controller; + +import static org.apache.coyote.http11.httpmessage.HttpHeaders.JSESSIONID; + +import java.util.Objects; + +import org.apache.catalina.servlet.AbstractController; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.request.HttpRequestParameters; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; +import org.apache.coyote.http11.httpmessage.response.StaticResource; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; + +public class LoginController extends AbstractController { + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws Exception { + HttpRequestParameters requestParams = HttpRequestParameters.parseFrom(request.getBody()); + String account = requestParams.getParam("account"); + String password = requestParams.getParam("password"); + User user = InMemoryUserRepository.fetchByAccount(account); + if (user.checkPassword(password)) { + Session session = request.getSession(true); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + + response.addCookie(JSESSIONID, session.getId()); + response.setStatusFound("/index.html"); + return; + } + + response.setStatusFound("/401.html"); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws Exception { + if (isLoggedIn(request)) { + response.setStatusFound("/index.html"); + return; + } + response.setResponseOfStaticResource(new StaticResource("/login.html")); + } + + private boolean isLoggedIn(HttpRequest httpRequest) { + return Objects.nonNull(httpRequest.getSession(false)); + } +} 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..701643895c --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,42 @@ +package com.techcourse.controller; + +import org.apache.catalina.servlet.AbstractController; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.request.HttpRequestParameters; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; +import org.apache.coyote.http11.httpmessage.response.StaticResource; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; + +public class RegisterController extends AbstractController { + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws Exception { + HttpRequestParameters methodRequest = HttpRequestParameters.parseFrom(request.getBody()); + User user = register(methodRequest); + Session session = request.getSession(true); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + + response.addCookie("JSESSIONID", session.getId()); + response.setStatusFound("/index.html"); + } + + private User register(HttpRequestParameters requestParams) { + String account = requestParams.getParam("account"); + User user = new User( + account, + requestParams.getParam("password"), + requestParams.getParam("email") + ); + InMemoryUserRepository.save(user); + return InMemoryUserRepository.fetchByAccount(account); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws Exception { + response.setResponseOfStaticResource(new StaticResource("/register.html")); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/StaticResourceController.java b/tomcat/src/main/java/com/techcourse/controller/StaticResourceController.java new file mode 100644 index 0000000000..2bab1ab70f --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/StaticResourceController.java @@ -0,0 +1,27 @@ +package com.techcourse.controller; + +import org.apache.catalina.servlet.AbstractController; +import org.apache.coyote.http11.exception.CantHandleRequestException; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; +import org.apache.coyote.http11.httpmessage.response.StaticResource; + +public class StaticResourceController extends AbstractController { + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws Exception { + throw new CantHandleRequestException( + String.format("%s %s 요청을 처리할 수 없습니다.", request.getMethod().name(), request.getTarget()) + ); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws Exception { + if (request.getTarget().equals("/")) { + StaticResource staticResource = new StaticResource("/index.html"); + response.setResponseOfStaticResource(staticResource); + return; + } + StaticResource staticResource = new StaticResource(request.getTarget()); + response.setResponseOfStaticResource(staticResource); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index fd850c6558..4c5392254c 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -2,7 +2,7 @@ import java.io.IOException; -import org.apache.coyote.http11.session.Session; +import org.apache.coyote.session.Session; /** * A Manager manages the pool of Sessions that are associated with a 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 2e2de79386..447c9853cd 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -5,6 +5,7 @@ import java.net.ServerSocket; import java.net.Socket; +import org.apache.coyote.ServletContainer; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,13 +18,16 @@ public class Connector implements Runnable { private static final int DEFAULT_ACCEPT_COUNT = 100; private final ServerSocket serverSocket; + private final ServletContainer servletContainer; + private boolean stopped; - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + public Connector(ServletContainer servletContainer) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, servletContainer); } - public Connector(int port, int acceptCount) { + public Connector(int port, int acceptCount, ServletContainer servletContainer) { + this.servletContainer = servletContainer; this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; } @@ -66,7 +70,7 @@ private void process(Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); + var processor = new Http11Processor(servletContainer, connection); new Thread(processor).start(); } diff --git a/tomcat/src/main/java/org/apache/catalina/exception/NoMatchedControllerException.java b/tomcat/src/main/java/org/apache/catalina/exception/NoMatchedControllerException.java new file mode 100644 index 0000000000..851114b214 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/exception/NoMatchedControllerException.java @@ -0,0 +1,7 @@ +package org.apache.catalina.exception; + +public class NoMatchedControllerException extends RuntimeException { + public NoMatchedControllerException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/servlet/AbstractController.java b/tomcat/src/main/java/org/apache/catalina/servlet/AbstractController.java new file mode 100644 index 0000000000..a71171118e --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/servlet/AbstractController.java @@ -0,0 +1,21 @@ +package org.apache.catalina.servlet; + +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + if (request.isGet()) { + doGet(request, response); + } + if (request.isPost()) { + doPost(request, response); + } + } + + protected abstract void doPost(HttpRequest request, HttpResponse response) throws Exception; + + protected abstract void doGet(HttpRequest request, HttpResponse response) throws Exception; +} diff --git a/tomcat/src/main/java/org/apache/catalina/servlet/Controller.java b/tomcat/src/main/java/org/apache/catalina/servlet/Controller.java new file mode 100644 index 0000000000..e873e618ee --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/servlet/Controller.java @@ -0,0 +1,8 @@ +package org.apache.catalina.servlet; + +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; + +public interface Controller { + void service(HttpRequest request, HttpResponse response) throws Exception; +} diff --git a/tomcat/src/main/java/org/apache/catalina/servlet/PathMatchServletContainer.java b/tomcat/src/main/java/org/apache/catalina/servlet/PathMatchServletContainer.java new file mode 100644 index 0000000000..23fb280aad --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/servlet/PathMatchServletContainer.java @@ -0,0 +1,19 @@ +package org.apache.catalina.servlet; + +import org.apache.coyote.ServletContainer; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; + +public class PathMatchServletContainer implements ServletContainer { + private final RequestMapping requestMapping; + + public PathMatchServletContainer(RequestMapping requestMapping) { + this.requestMapping = requestMapping; + } + + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + Controller controller = requestMapping.getController(request); + controller.service(request, response); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/servlet/RequestMapping.java b/tomcat/src/main/java/org/apache/catalina/servlet/RequestMapping.java new file mode 100644 index 0000000000..dbc7cdef21 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/servlet/RequestMapping.java @@ -0,0 +1,24 @@ +package org.apache.catalina.servlet; + +import java.util.Map; + +import org.apache.catalina.exception.NoMatchedControllerException; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; + +public class RequestMapping { + private final Map mappedController; + + public RequestMapping(Map mappedController) { + this.mappedController = mappedController; + } + + public Controller getController(HttpRequest request) { + String controllerKey = mappedController.keySet().stream() + .filter(pathRegex -> request.getTarget().matches(pathRegex)) + .findFirst() + .orElseThrow(() -> new NoMatchedControllerException( + request.getTarget() + " 요청을 처리할 컨트롤러가 존재하지 않습니다.") + ); + return mappedController.get(controllerKey); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java index 61afa8df71..eddcdf8b0c 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -3,6 +3,7 @@ import java.io.IOException; import org.apache.catalina.connector.Connector; +import org.apache.coyote.ServletContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,8 +11,14 @@ public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + private final ServletContainer servletContainer; + + public Tomcat(ServletContainer servletContainer) { + this.servletContainer = servletContainer; + } + public void start() { - var connector = new Connector(); + var connector = new Connector(servletContainer); connector.start(); try { diff --git a/tomcat/src/main/java/org/apache/coyote/ServletContainer.java b/tomcat/src/main/java/org/apache/coyote/ServletContainer.java new file mode 100644 index 0000000000..dafc782b31 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/ServletContainer.java @@ -0,0 +1,8 @@ +package org.apache.coyote; + +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; + +public interface ServletContainer { + void service(HttpRequest request, HttpResponse response) throws Exception; +} 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 e61d416570..cd60a86516 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -6,8 +6,9 @@ import java.net.Socket; import org.apache.coyote.Processor; -import org.apache.coyote.http11.handler.DefaultResourceHandler; -import org.apache.coyote.http11.httpmessage.request.Request; +import org.apache.coyote.ServletContainer; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,12 +17,12 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private final RequestHandler requestHandler; + private final ServletContainer servletContainer; private final Socket connection; - public Http11Processor(Socket connection) { + public Http11Processor(ServletContainer servletContainer, Socket connection) { + this.servletContainer = servletContainer; this.connection = connection; - this.requestHandler = new DefaultResourceHandler(); } @Override @@ -37,18 +38,29 @@ public void process(Socket connection) { BufferedReader requestBufferedReader = new BufferedReader(inputStreamReader); var outputStream = connection.getOutputStream()) { - Request request = Request.readFrom(requestBufferedReader); - log.info("request : {}", request); - String response = getResponse(request); + HttpRequest httpRequest = HttpRequest.readFrom(requestBufferedReader); + log.info("request : {}", httpRequest); + HttpResponse httpResponse = new HttpResponse(httpRequest); - outputStream.write(response.getBytes()); + service(httpRequest, httpResponse); + + outputStream.write(httpResponse.toHttpMessage().getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } - private String getResponse(Request request) throws IOException { - return requestHandler.handle(request); + private void service(HttpRequest httpRequest, HttpResponse httpResponse) { + try { + servletContainer.service(httpRequest, httpResponse); + if(!httpResponse.containsHeader("Cache-Control")) { + httpResponse.addHeader("Cache-Control", "no-cache, private"); + } + + } catch (Exception e) { + log.error("request 처리 실패 : ", e); + httpResponse.setStatusBadRequest(); + } } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java deleted file mode 100644 index 89ac542ac2..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.IOException; - -import org.apache.coyote.http11.httpmessage.request.Request; - -@FunctionalInterface -public interface RequestHandler { - String handle(Request request) throws IOException; -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java deleted file mode 100644 index 922fbc0c82..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/exception/CanNotHandleRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.apache.coyote.http11.exception; - -public class CanNotHandleRequest extends RuntimeException { - public CanNotHandleRequest(String message) { - super(message); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/CantHandleRequestException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/CantHandleRequestException.java new file mode 100644 index 0000000000..f81aa9cff8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/CantHandleRequestException.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class CantHandleRequestException extends RuntimeException { + public CantHandleRequestException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/IllegalHttpMessageException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/IllegalHttpMessageException.java new file mode 100644 index 0000000000..206ed000b1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/IllegalHttpMessageException.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class IllegalHttpMessageException extends RuntimeException { + public IllegalHttpMessageException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/NoHandlerException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/NoHandlerException.java deleted file mode 100644 index a8f36e5d4e..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/exception/NoHandlerException.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.apache.coyote.http11.exception; - -public class NoHandlerException extends RuntimeException { - public NoHandlerException(String message) { - super(message); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java deleted file mode 100644 index fcca082d83..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/DefaultResourceHandler.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.apache.coyote.http11.handler; - -import java.io.IOException; -import java.util.Objects; - -import org.apache.coyote.http11.RequestHandler; -import org.apache.coyote.http11.exception.CanNotHandleRequest; -import org.apache.coyote.http11.exception.NoSuchUserException; -import org.apache.coyote.http11.httpmessage.request.Request; -import org.apache.coyote.http11.httpmessage.response.Response; -import org.apache.coyote.http11.httpmessage.response.StaticResource; -import org.apache.coyote.http11.session.Session; -import org.apache.coyote.http11.session.SessionManager; - -import com.techcourse.db.InMemoryUserRepository; -import com.techcourse.model.User; - -public class DefaultResourceHandler implements RequestHandler { - - @Override - public String handle(Request request) throws IOException { - if (request.isStaticResourceRequest()) { - return Response.builder() - .versionOf(request.getHttpVersion()) - .ofStaticResource(new StaticResource(request.getTarget())) - .toHttpMessage(); - } - if (request.getTarget().equals("/")) { - return Response.builder() - .versionOf(request.getHttpVersion()) - .ofStaticResource(new StaticResource("/index.html")) - .toHttpMessage(); - } - if (request.getTarget().equals("/login")) { - return loginResponse(request); - } - if (request.getTarget().contains("register")) { - return registerResponse(request); - } - throw new CanNotHandleRequest("처리할 수 없는 요청입니다. : " + request.getTarget()); - } - - private String loginResponse(Request request) throws IOException { - if (request.isPost()) { - return login(request).toHttpMessage(); - } - if (isLoggedIn(request)) { - return Response.builder() - .versionOf(request.getHttpVersion()) - .found("/index.html") - .toHttpMessage(); - } - return Response.builder() - .versionOf(request.getHttpVersion()) - .ofStaticResource(new StaticResource("/login.html")) - .toHttpMessage(); - } - - private boolean isLoggedIn(Request request) { - return Objects.nonNull(request.getSession(false)); - } - - private Response login(Request request) throws NoSuchUserException { - RequestParameters requestParams = RequestParameters.parseFrom(request.getBody()); - String account = requestParams.getParam("account"); - String password = requestParams.getParam("password"); - User user = InMemoryUserRepository.fetchByAccount(account); - if (user.checkPassword(password)) { - Session session = request.getSession(true); - session.setAttribute("user", user); - SessionManager.getInstance().add(session); - return Response.builder() - .versionOf(request.getHttpVersion()) - .addCookie("JSESSIONID", session.getId()) - .found("/index.html"); - } - - return Response.builder() - .versionOf(request.getHttpVersion()) - .found("/401.html"); - } - - private String registerResponse(Request request) throws IOException { - if (request.isPost()) { - RequestParameters methodRequest = RequestParameters.parseFrom(request.getBody()); - User user = register(methodRequest); - Session session = request.getSession(true); - session.setAttribute("user", user); - SessionManager.getInstance().add(session); - return Response.builder() - .versionOf(request.getHttpVersion()) - .addCookie("JSESSIONID", session.getId()) - .found("/index.html") - .toHttpMessage(); - } - - return Response.builder() - .versionOf(request.getHttpVersion()) - .ofStaticResource(new StaticResource("/register.html")) - .toHttpMessage(); - } - - private User register(RequestParameters requestParams) { - String account = requestParams.getParam("account"); - User user = new User( - account, - requestParams.getParam("password"), - requestParams.getParam("email") - ); - InMemoryUserRepository.save(user); - return InMemoryUserRepository.fetchByAccount(account); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpMethod.java similarity index 81% rename from tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java rename to tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpMethod.java index 46e3b4ed9a..995138f632 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Method.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpMethod.java @@ -2,10 +2,10 @@ import java.util.Arrays; -public enum Method { +public enum HttpMethod { GET, POST, PUT, PATCH, DELETE; - public static Method findByName(String name) { + public static HttpMethod findByName(String name) { return Arrays.stream(values()) .filter(method -> method.name().equals(name)) .findFirst() diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequest.java similarity index 73% rename from tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java rename to tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequest.java index 83798f2c7d..f0db2f9b5f 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequest.java @@ -8,20 +8,20 @@ import org.apache.coyote.http11.httpmessage.HttpCookie; import org.apache.coyote.http11.httpmessage.HttpHeaders; -import org.apache.coyote.http11.session.Session; -import org.apache.coyote.http11.session.SessionManager; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; -public class Request { +public class HttpRequest { - private final RequestLine requestLine; + private final HttpRequestLine httpRequestLine; private final HttpHeaders headers; private final HttpCookie cookies; private final String body; private Session session; - private Request(RequestLine requestLine, HttpHeaders headers, String body) { - this.requestLine = requestLine; + private HttpRequest(HttpRequestLine httpRequestLine, HttpHeaders headers, String body) { + this.httpRequestLine = httpRequestLine; this.headers = headers; if (headers.contains(HttpHeaders.COOKIE)) { this.cookies = HttpCookie.parseFrom(headers.get(HttpHeaders.COOKIE)); @@ -36,11 +36,11 @@ private Request(RequestLine requestLine, HttpHeaders headers, String body) { } } - public static Request readFrom(BufferedReader reader) throws IOException { - RequestLine requestLine = RequestLine.parseFrom(reader.readLine()); + public static HttpRequest readFrom(BufferedReader reader) throws IOException { + HttpRequestLine httpRequestLine = HttpRequestLine.parseFrom(reader.readLine()); HttpHeaders header = new HttpHeaders(readHeader(reader)); String requestBody = readBody(reader, header.getContentLength()); - return new Request(requestLine, header, requestBody); + return new HttpRequest(httpRequestLine, header, requestBody); } private static Map readHeader(BufferedReader reader) throws IOException { @@ -70,24 +70,24 @@ public Session getSession(boolean created) { return this.session; } - public boolean isPost() { - return requestLine.isPost(); + public boolean isGet() { + return httpRequestLine.isGet(); } - public boolean isStaticResourceRequest() { - return requestLine.isStaticResourceRequest(); + public boolean isPost() { + return httpRequestLine.isPost(); } - public Method getMethod() { - return requestLine.method(); + public HttpMethod getMethod() { + return httpRequestLine.httpMethod(); } public String getTarget() { - return requestLine.target(); + return httpRequestLine.target(); } public String getHttpVersion() { - return requestLine.httpVersion(); + return httpRequestLine.httpVersion(); } public String getBody() { @@ -97,7 +97,7 @@ public String getBody() { @Override public String toString() { return "Request{" + - "requestLine=" + requestLine + + "requestLine=" + httpRequestLine + ", headers=" + headers + ", body='" + body + '\'' + '}'; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestLine.java new file mode 100644 index 0000000000..e3cc362f9a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestLine.java @@ -0,0 +1,41 @@ +package org.apache.coyote.http11.httpmessage.request; + +import org.apache.coyote.http11.exception.IllegalHttpMessageException; + +public record HttpRequestLine( + HttpMethod httpMethod, + String target, + String httpVersion +) { + + public static HttpRequestLine parseFrom(String httpRequestLineText) { + String[] token = httpRequestLineText.split(" "); + if (token.length != 3) { + throw new IllegalHttpMessageException("잘못된 헤더 형식입니다."); + } + return new HttpRequestLine(HttpMethod.findByName(token[0]), token[1], token[2]); + } + + public boolean isGet() { + return this.httpMethod == HttpMethod.GET; + } + + public boolean isPost() { + return httpMethod == HttpMethod.POST; + } + + public boolean isStaticResourceRequest() { + return target.contains(".css") || + target.contains(".html") || + target.contains(".js"); + } + + @Override + public String toString() { + return "RequestLine{" + + "httpMethod=" + httpMethod + + ", target='" + target + '\'' + + ", httpVersion='" + httpVersion + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestParameters.java similarity index 72% rename from tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java rename to tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestParameters.java index f75590253a..0494ff1720 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/handler/RequestParameters.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/HttpRequestParameters.java @@ -1,25 +1,25 @@ -package org.apache.coyote.http11.handler; +package org.apache.coyote.http11.httpmessage.request; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -public class RequestParameters { +public class HttpRequestParameters { private final Map requestParams; - private RequestParameters(Map requestParams) { + private HttpRequestParameters(Map requestParams) { this.requestParams = Map.copyOf(requestParams); } - public static RequestParameters parseFrom(String paramString) { + public static HttpRequestParameters parseFrom(String paramString) { Map requestParams = new HashMap<>(); String[] requestParamTokens = paramString.split("&"); for (String requestParam : requestParamTokens) { String[] split = requestParam.split("="); requestParams.put(split[0], split[1]); } - return new RequestParameters(requestParams); + return new HttpRequestParameters(requestParams); } public String getParam(String key) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java deleted file mode 100644 index 66cad8037e..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/request/RequestLine.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apache.coyote.http11.httpmessage.request; - -public record RequestLine ( - Method method, - String target, - String httpVersion -) { - - public static RequestLine parseFrom(String requestLineText) { - String[] token = requestLineText.split(" "); - return new RequestLine(Method.findByName(token[0]), token[1], token[2]); - } - - public boolean isPost() { - return method == Method.POST; - } - - public boolean isStaticResourceRequest() { - return target.contains(".css") || - target.contains(".html") || - target.contains(".js"); - } - - @Override - public String toString() { - return "RequestLine{" + - "method=" + method + - ", target='" + target + '\'' + - ", httpVersion='" + httpVersion + '\'' + - '}'; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpResponse.java new file mode 100644 index 0000000000..1f8829ac82 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpResponse.java @@ -0,0 +1,87 @@ +package org.apache.coyote.http11.httpmessage.response; + +import java.io.IOException; + +import org.apache.coyote.http11.exception.NotCompleteResponseException; +import org.apache.coyote.http11.httpmessage.HttpCookie; +import org.apache.coyote.http11.httpmessage.HttpHeaders; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; + +public class HttpResponse { + + private final HttpCookie httpCookie; + private final HttpStatusLine httpStatusLine; + private final HttpHeaders headers; + private String body; + + + public HttpResponse(HttpStatusLine httpStatusLine, HttpHeaders headers, String body) { + this.httpStatusLine = httpStatusLine; + this.headers = headers; + this.body = body; + httpCookie = new HttpCookie(); + } + + public HttpResponse(HttpRequest httpRequest) { + + this(new HttpStatusLine(httpRequest.getHttpVersion(), HttpStatus.NOT_FOUND), + new HttpHeaders(), + ""); + } + + public boolean containsHeader(String headerName) { + return headers.contains(headerName); + } + + public void addHeader(String name, String value) { + headers.addHeader(name, value); + } + + public String toHttpMessage() { + if (httpStatusLine == null) { + throw new NotCompleteResponseException("응답이 완성되지 않았습니다."); + } + setCookie(); + return String.join("\r\n", + httpStatusLine.toHttpMessage(), + headers.toHttpMessage(), + "", + body); + } + + private void setCookie() { + if(!httpCookie.isEmpty()) { + headers.addHeader(HttpHeaders.SET_COOKIE, httpCookie.toHttpMessage()); + } + } + + public void setStatusFound(String target) { + this.httpStatusLine.setStatus(HttpStatus.FOUND); + this.headers.addHeader(HttpHeaders.LOCATION, target); + } + + public void setStatusBadRequest() { + this.httpStatusLine.setStatus(HttpStatus.BAD_REQUEST); + } + + public void setResponseOfStaticResource(StaticResource resource) throws IOException { + this.httpStatusLine.setStatus(HttpStatus.OK); + headers.addHeader(HttpHeaders.CONTENT_TYPE, resource.getContentType() + ";charset=utf-8"); + headers.addHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(resource.getContentLength())); + this.body = resource.getContent(); + } + + public void addCookie(String key, String value) { + httpCookie.addCookie(key, value); + } + + @Override + public String toString() { + return "HttpResponse{" + + "httpCookie=" + httpCookie + + ", httpStatusLine=" + httpStatusLine + + ", headers=" + headers + + ", body='" + body + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatus.java new file mode 100644 index 0000000000..e1595c8a08 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatus.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.httpmessage.response; + +public enum HttpStatus { + OK(200, "OK"), + FOUND(301, "FOUND"), + BAD_REQUEST(400, "BAD REQUEST"), + NOT_FOUND(404, "NOT FOUND"); + + private final int statusCode; + private final String statusText; + + HttpStatus(int statusCode, String statusText) { + this.statusCode = statusCode; + this.statusText = statusText; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusText() { + return statusText; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatusLine.java new file mode 100644 index 0000000000..b0f4e0f376 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/HttpStatusLine.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.httpmessage.response; + +public class HttpStatusLine { + + private final String httpVersion; + + private int statusCode; + private String statusText; + + public HttpStatusLine(String httpVersion, HttpStatus httpStatus) { + this.httpVersion = httpVersion; + this.statusCode = httpStatus.getStatusCode(); + this.statusText = httpStatus.getStatusText(); + } + + public String toHttpMessage() { + return String.format("%s %s %s ", httpVersion, statusCode, statusText); + } + + public void setStatus(HttpStatus status) { + this.statusCode = status.getStatusCode(); + this.statusText = status.getStatusText(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java deleted file mode 100644 index 13e65f1303..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/Response.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.apache.coyote.http11.httpmessage.response; - -import java.io.IOException; - -import org.apache.coyote.http11.exception.NotCompleteResponseException; -import org.apache.coyote.http11.httpmessage.HttpCookie; -import org.apache.coyote.http11.httpmessage.HttpHeaders; - -public class Response { - - private final StatusLine statusLine; - private final HttpHeaders headers; - private final String content; - - public static ResponseBuilder builder() { - return new ResponseBuilder(); - } - - public Response(StatusLine statusLine, HttpHeaders headers, String content) { - this.statusLine = statusLine; - this.headers = headers; - this.content = content; - } - - public static class ResponseBuilder { - private final HttpCookie httpCookie; - private final HttpHeaders headers; - private String ProtocolVersion; - - private ResponseBuilder() { - ProtocolVersion = "HTTP/1.1"; - httpCookie = new HttpCookie(); - headers = new HttpHeaders(); - } - - public ResponseBuilder versionOf(String protocolVersion) { - this.ProtocolVersion = protocolVersion; - return this; - } - - public ResponseBuilder addCookie(String key, String value) { - httpCookie.addCookie(key, value); - return this; - } - - public Response found(String target) { - this.headers.addHeader(HttpHeaders.LOCATION, target); - - return build( - new StatusLine(this.ProtocolVersion, 301, "FOUND"), - this.headers, - "" - ); - } - - public Response ofStaticResource(StaticResource resource) throws IOException { - headers.addHeader(HttpHeaders.CONTENT_TYPE, resource.getContentType() + ";charset=utf-8"); - headers.addHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(resource.getContentLength())); - - return build( - new StatusLine(this.ProtocolVersion, 200, "OK"), - this.headers, - resource.getContent() - ); - } - - public Response build(StatusLine statusLine, HttpHeaders headers, String content) { - setCookie(headers); - return new Response(statusLine, headers, content); - } - - private void setCookie(HttpHeaders headers) { - if(!httpCookie.isEmpty()) { - headers.addHeader(HttpHeaders.SET_COOKIE, httpCookie.toHttpMessage()); - } - } - } - - public String toHttpMessage() { - if (statusLine == null) { - throw new NotCompleteResponseException("응답이 완성되지 않았습니다."); - } - return String.join("\r\n", - statusLine.toHttpMessage(), - headers.toHttpMessage(), - "", - content); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java deleted file mode 100644 index 21b1a87990..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/httpmessage/response/StatusLine.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.apache.coyote.http11.httpmessage.response; - -public class StatusLine { - - private final String protocolVersion; - private final int statusCode; - private final String statusText; - - public StatusLine(String protocolVersion, int statusCode, String statusText) { - this.protocolVersion = protocolVersion; - this.statusCode = statusCode; - this.statusText = statusText; - } - - public String toHttpMessage() { - return String.format("%s %s %s ", protocolVersion, statusCode, statusText); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java b/tomcat/src/main/java/org/apache/coyote/session/Session.java similarity index 93% rename from tomcat/src/main/java/org/apache/coyote/http11/session/Session.java rename to tomcat/src/main/java/org/apache/coyote/session/Session.java index ba63df869d..c1c2f41b55 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java +++ b/tomcat/src/main/java/org/apache/coyote/session/Session.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.session; +package org.apache.coyote.session; import java.util.HashMap; import java.util.Map; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java similarity index 94% rename from tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java rename to tomcat/src/main/java/org/apache/coyote/session/SessionManager.java index 277c87c3e1..482a7a8967 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java +++ b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.session; +package org.apache.coyote.session; import java.util.HashMap; import java.util.Map; diff --git a/tomcat/src/test/java/org/apache/catalina/TestPathServletContainerConfig.java b/tomcat/src/test/java/org/apache/catalina/TestPathServletContainerConfig.java new file mode 100644 index 0000000000..a636d3678a --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/TestPathServletContainerConfig.java @@ -0,0 +1,37 @@ +package org.apache.catalina; + +import java.util.Map; + +import org.apache.catalina.servlet.AbstractController; +import org.apache.catalina.servlet.RequestMapping; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; +import org.apache.coyote.http11.httpmessage.response.StaticResource; + +import com.techcourse.controller.StaticResourceController; + +public class TestPathServletContainerConfig { + + public static final String SUCCESS_PATH = "/success"; + + public static RequestMapping getRequestMapping() { + return new RequestMapping( + Map.of( + SUCCESS_PATH, new SuccessController() + ) + ); + } + + private static class SuccessController extends AbstractController { + + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws Exception { + response.setResponseOfStaticResource(new StaticResource("/index.html")); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws Exception { + response.setResponseOfStaticResource(new StaticResource("/index.html")); + } + } +} diff --git a/tomcat/src/test/java/org/apache/catalina/servlet/PathMatchServletContainerTest.java b/tomcat/src/test/java/org/apache/catalina/servlet/PathMatchServletContainerTest.java new file mode 100644 index 0000000000..f840e3432b --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/servlet/PathMatchServletContainerTest.java @@ -0,0 +1,55 @@ +package org.apache.catalina.servlet; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.catalina.TestPathServletContainerConfig; +import org.apache.catalina.exception.NoMatchedControllerException; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.http11.httpmessage.response.HttpResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PathMatchServletContainerTest { + + private PathMatchServletContainer pathMatchServletContainer = new PathMatchServletContainer( + TestPathServletContainerConfig.getRequestMapping() + ); + + @Test + @DisplayName("매핑 되는 경로의 컨트롤러가 존재하면 요청을 처리할 수 있다.") + void successService() throws IOException { + //given + HttpRequest httpRequest = makeRequestFrom("GET /success HTTP/1.1 "); + HttpResponse httpResponse = new HttpResponse(httpRequest); + + //when & then + assertThatCode(() -> pathMatchServletContainer.service(httpRequest, httpResponse)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("매핑 되는 경로의 컨트롤러가 존재하지 않으면 예외가 발생한다.") + void failServiceNotMatchController() throws IOException { + //given + HttpRequest httpRequest = makeRequestFrom("GET /fail HTTP/1.1 "); + HttpResponse httpResponse = new HttpResponse(httpRequest); + + //when & then + assertThatThrownBy(() -> pathMatchServletContainer.service(httpRequest, httpResponse)) + .isInstanceOf(NoMatchedControllerException.class) + .hasMessage(httpRequest.getTarget() + " 요청을 처리할 컨트롤러가 존재하지 않습니다."); + } + + private HttpRequest makeRequestFrom(String requestMessage) throws IOException { + var bufferedInputStream = new BufferedReader( + new InputStreamReader(new ByteArrayInputStream(requestMessage.getBytes())) + ); + return HttpRequest.readFrom(bufferedInputStream); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index a521e0059a..ad21dbbbe4 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -7,13 +7,17 @@ import java.net.URL; import java.nio.file.Files; -import org.apache.coyote.http11.session.Session; -import org.apache.coyote.http11.session.SessionManager; +import org.apache.catalina.servlet.PathMatchServletContainer; +import org.apache.catalina.servlet.RequestMapping; +import org.apache.coyote.ServletContainer; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import com.techcourse.config.RequestMappingConfig; import com.techcourse.db.InMemoryUserRepository; import com.techcourse.model.User; @@ -21,12 +25,15 @@ class Http11ProcessorTest { + private final RequestMapping requestMapping = RequestMappingConfig.getRequestMapping(); + private final ServletContainer servletContainer = new PathMatchServletContainer(requestMapping); + @Test @Disabled void process() { // given final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final var processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -51,7 +58,7 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -85,7 +92,7 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -115,7 +122,7 @@ void css() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -149,7 +156,7 @@ void login() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -184,7 +191,7 @@ void loginViewWhenLoggedIn() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -207,7 +214,7 @@ void loginSuccess() throws IOException { "", requestBody); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -232,7 +239,7 @@ void loginFailed() throws IOException { requestBody); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -260,7 +267,7 @@ void registerView() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); @@ -290,7 +297,7 @@ void registerSuccess() throws IOException { requestBody); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(servletContainer, socket); // when processor.process(socket); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestLineTest.java new file mode 100644 index 0000000000..ce36c0eb81 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestLineTest.java @@ -0,0 +1,95 @@ +package org.apache.coyote.http11.httpmessage.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.coyote.http11.exception.IllegalHttpMessageException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class HttpHttpRequestLineTest { + + @Nested + @DisplayName("파싱 테스트") + class ParseFromTest { + @Test + @DisplayName("파싱 성공") + void success() { + // given + String requestLineText = "GET /index.html HTTP/1.1 "; + + // when + HttpRequestLine httpRequestLine = HttpRequestLine.parseFrom(requestLineText); + + //then + assertAll( + () -> assertThat(httpRequestLine.httpVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(httpRequestLine.httpMethod()).isEqualTo(HttpMethod.GET), + () -> assertThat(httpRequestLine.target()).isEqualTo("/index.html") + ); + } + + @Test + @DisplayName("잘못된 형식 파싱 실패") + void IllegalFormTest() { + // given + String wrongRequestLine = "GET /index.html"; + + // when & then + assertThatThrownBy(() -> HttpRequestLine.parseFrom(wrongRequestLine)) + .isInstanceOf(IllegalHttpMessageException.class) + .hasMessageContaining("잘못된 헤더 형식입니다."); + } + } + + @Nested + @DisplayName("정적 리소스 확인 테스트") + class IsStaticResourceHttpRequestTest { + + @ParameterizedTest(name = "{0} 확장자 파일을 요청하면 정적 리소스로 판단한다.") + @ValueSource(strings = {".css", ".html", ".js"}) + void staticResourceTest(String extension) { + // given + String target = "/somePath" + extension; + String requestLineText = "GET " + target + " HTTP/1.1"; + + //when + HttpRequestLine httpRequestLine = HttpRequestLine.parseFrom(requestLineText); + + //then + assertThat(httpRequestLine.isStaticResourceRequest()).isTrue(); + } + + @Test + @DisplayName("파일 확장자가 없으면 정적 리소스로 판단하지 않는다.") + void noExtensionPathTest() { + // given + String target = "/somePath"; + String requestLineText = "GET " + target + " HTTP/1.1"; + + //when + HttpRequestLine httpRequestLine = HttpRequestLine.parseFrom(requestLineText); + + //then + assertThat(httpRequestLine.isStaticResourceRequest()).isFalse(); + } + + @Test + @DisplayName("css, js, html 이외의 파일 확장자가 있으면 정적 리소스로 판단하지 않는다.") + void otherExtensionPathTest() { + // given + String target = "/somePath.java"; + String requestLineText = "GET " + target + " HTTP/1.1"; + + //when + HttpRequestLine httpRequestLine = HttpRequestLine.parseFrom(requestLineText); + + //then + assertThat(httpRequestLine.isStaticResourceRequest()).isFalse(); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestParametersTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestParametersTest.java new file mode 100644 index 0000000000..3f21357596 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpHttpRequestParametersTest.java @@ -0,0 +1,64 @@ +package org.apache.coyote.http11.httpmessage.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class HttpHttpRequestParametersTest { + + @Nested + @DisplayName("파싱 테스트") + class ParseTest { + + @Test + @DisplayName("key=value 형식의 문자열을 정상적으로 파싱할 수 있다.") + void oneKeyValueFromParseTest() { + //given + String parameter = "key=value"; + + //when + HttpRequestParameters httpRequestParameters = HttpRequestParameters.parseFrom(parameter); + + //then + assertThat(httpRequestParameters.getParam("key")).isEqualTo("value"); + } + + @ParameterizedTest + @DisplayName("key=value 형식의 문자열 여러개를 & 로 이어 붙이면 정상적으로 파싱할 수 있다.") + @ValueSource(strings = {"key1=value1&key2=value2", "key1=value1&key2=value2&key3=value3"}) + void multipleKeyValueFromParseTest(String parameterText) { + assertThatCode(() -> HttpRequestParameters.parseFrom(parameterText)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest(name = "{0}을 이용해 문자열을 이어붙이면 제대로 파싱되지 않는다.") + @ValueSource(strings = {",", "|", ";"}) + void wrongDelimiterTest(String delimiter) { + //given + String parameterText = String.join(delimiter, "key1=value1", + "key2=value2"); + + //when + HttpRequestParameters httpRequestParameters = HttpRequestParameters.parseFrom(parameterText); + + //then + assertAll( + () -> assertThat(httpRequestParameters.getParam("key1")) + .isNotEqualTo("value1"), + () -> assertThatThrownBy(() -> httpRequestParameters.getParam("key2")) + .isInstanceOf(NoSuchElementException.class) + .hasMessage("key2 에 해당하는 값이 존재하지 않습니다.") + ); + + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpMethodTest.java b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpMethodTest.java new file mode 100644 index 0000000000..b83201eb4b --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/httpmessage/request/HttpMethodTest.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.httpmessage.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class HttpMethodTest { + + @Nested + @DisplayName("이름 조회 테스트") + class FindByName { + + @ParameterizedTest + @EnumSource(value = HttpMethod.class) + @DisplayName("Http 메서드 이름으로 HttpMethod 객체를 조회할 수 있다.") + void successTest(HttpMethod givenMethod) { + HttpMethod findMethod = HttpMethod.findByName(givenMethod.name()); + + assertThat(findMethod).isEqualTo(givenMethod); + } + + @Test + @DisplayName("잘못된 이름으로 HttpMethod 를 조회하면 예외가 발생한다.") + void wrongNameTest() { + assertThatThrownBy(() -> HttpMethod.findByName("wrongName")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 Http 메서드 입니다."); + } + + @Test + @DisplayName("null 로 HttpMethod 를 조회하면 예외가 발생한다.") + void nullNameTest() { + assertThatThrownBy(() -> HttpMethod.findByName(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("잘못된 Http 메서드 입니다."); + } + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..6c9eada100 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,138 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.coyote.http11.httpmessage.request.HttpMethod; +import org.apache.coyote.http11.httpmessage.request.HttpRequest; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class HttpRequestTest { + + @Nested + @DisplayName("생성 테스트") + class ConstructorTest { + + @Test + @DisplayName("요청 생성 테스트") + void getRequestConstructTest() throws IOException { + //given + String requestBody = "requestBody"; + String requestMessage = String.join("\r\n", + "POST /target HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: " + requestBody.getBytes().length, + "", + requestBody); + + //when + HttpRequest request = createRequest(requestMessage); + + //then + assertAll( + () -> assertThat(request.getMethod()).isEqualTo(HttpMethod.POST), + () -> assertThat(request.getTarget()).isEqualTo("/target"), + () -> assertThat(request.getHttpVersion()).isEqualTo("HTTP/1.1"), + () -> assertThat(request.getBody()).isEqualTo(requestBody) + ); + } + } + + @Nested + @DisplayName("세션 생성 테스트") + class SessionTest { + + @Nested + @DisplayName("쿠키로 JSESSIONID 를 가지고 있는 HttpRequest 객체는") + class ContainCookieTest { + String requestMessage = String.join("\r\n", + "GET /target HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + "Cookie: JSESSIONID=1234", + ""); + + @Test + @DisplayName("세션을 조회할 수 있다.") + void findSessionTest() throws IOException { + //given + SessionManager.getInstance().add(new Session("1234")); + HttpRequest request = createRequest(requestMessage); + + //when + Session session = request.getSession(false); + + //then + assertThat(session.getId()).isEqualTo("1234"); + } + + @Test + @DisplayName("세션을 생성해도 원래의 세션이 조회된다.") + void createSessionTest() throws IOException { + //given + SessionManager.getInstance().add(new Session("1234")); + HttpRequest request = createRequest(requestMessage); + + //when + Session session = request.getSession(true); + + //then + assertThat(session.getId()).isEqualTo("1234"); + } + } + + @Nested + @DisplayName("쿠키로 JSESSIONID 를 가지고 있지 않은 HttpRequest 객체는") + class NotContainCookieTest { + String requestMessage = String.join("\r\n", + "GET /target HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + ""); + + @Test + @DisplayName("세션을 조회하면 null 이 반환된다.") + void findSessionTest() throws IOException { + //given + HttpRequest request = createRequest(requestMessage); + + //when + Session session = request.getSession(false); + + //then + assertThat(session).isNull(); + } + + @Test + @DisplayName("세션을 새로 생성할 수 있다.") + void createSessionTest() throws IOException { + //given + HttpRequest request = createRequest(requestMessage); + + //when + Session session = request.getSession(true); + + //then + assertThat(session.getId()).isNotNull(); + } + } + } + + private HttpRequest createRequest(String httpRequestText) throws IOException { + InputStream requestStream = new ByteArrayInputStream(httpRequestText.getBytes()); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(requestStream)); + + return HttpRequest.readFrom(bufferedReader); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java deleted file mode 100644 index 8438feca69..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.apache.coyote.http11.request; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -import org.apache.coyote.http11.httpmessage.request.Method; -import org.apache.coyote.http11.httpmessage.request.Request; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class RequestTest { - - @Nested - @DisplayName("생성 테스트") - class ConstructorTest { - - @Test - @DisplayName("GET 요청 생성 테스트") - void getRequestConstructTest() throws IOException { - //given - String input = """ - GET /index.html HTTP/1.1 - Host: localhost:8080 - Connection: keep-alive - - """; - - //when - InputStream requestStream = new ByteArrayInputStream(input.getBytes()); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(requestStream)); - - - Request request = Request.readFrom(bufferedReader); - - //then - assertAll( - () -> assertThat(request.getMethod()).isEqualTo(Method.GET), - () -> assertThat(request.getTarget()).isEqualTo("/index.html") - ); - } - } -} diff --git a/tomcat/src/test/java/org/apache/coyote/session/SessionManagerTest.java b/tomcat/src/test/java/org/apache/coyote/session/SessionManagerTest.java new file mode 100644 index 0000000000..86069c4c09 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/session/SessionManagerTest.java @@ -0,0 +1,45 @@ +package org.apache.coyote.session; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SessionManagerTest { + + @Test + @DisplayName("SessionManager 를 싱글톤으로 관리한다.") + void singletonTest() { + SessionManager sessionManager1 = SessionManager.getInstance(); + SessionManager sessionManager2 = SessionManager.getInstance(); + + assertThat(sessionManager1).isSameAs(sessionManager2); + } + + @Test + @DisplayName("Session 을 저장하고 session id 값으로 다시 조회할 수 있다.") + void sessionAddAndFindTest() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + SessionManager sessionManager = SessionManager.getInstance(); + + sessionManager.add(session); + + assertThat(sessionManager.findSession(sessionId)) + .isEqualTo(session); + } + + @Test + @DisplayName("Session 을 삭제하면 session id 값으로 조회했을 때 null 이 나온다.") + void sessionRemoveAndFindTest() { + String sessionId = "sessionId"; + Session session = new Session(sessionId); + SessionManager sessionManager = SessionManager.getInstance(); + + sessionManager.add(session); + sessionManager.remove(sessionId); + + assertThat(sessionManager.findSession(sessionId)) + .isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/session/SessionTest.java b/tomcat/src/test/java/org/apache/coyote/session/SessionTest.java new file mode 100644 index 0000000000..f8a5c0500f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/session/SessionTest.java @@ -0,0 +1,53 @@ +package org.apache.coyote.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SessionTest { + + @Test + @DisplayName("세션에 정보를 저장하고 조회할 수 있다.") + void sessionAddTest () { + Session session = new Session("id"); + + session.setAttribute("key", "value"); + + assertThat(session.getAttribute("key")) + .isEqualTo("value"); + } + + @Test + @DisplayName("세션에 정보를 삭제하면 정보 조회시 null 이 반환된다.") + void sessionAttributeRemoveTest () { + Session session = new Session("id"); + + session.setAttribute("key", "value"); + session.removeAttribute("key"); + + assertThat(session.getAttribute("key")) + .isNull(); + } + + @Test + @DisplayName("세션을 만료 시키면 모든 데이터가 삭제된다..") + void sessionInvalidateTest () { + Session session = new Session("id"); + + session.setAttribute("key1", "value1"); + session.setAttribute("key2", "value2"); + session.setAttribute("key3", "value3"); + session.invalidate(); + + assertAll( + () -> assertThat(session.getAttribute("key1")) + .isNull(), + () -> assertThat(session.getAttribute("key2")) + .isNull(), + () -> assertThat(session.getAttribute("key3")) + .isNull() + ); + } +}