From 20faf64f9543a6b9c8b177ee27c56725920de037 Mon Sep 17 00:00:00 2001 From: Miseong Kim Date: Wed, 13 Sep 2023 15:23:52 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=86=B0=EC=BA=A3=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20-=202,=203,=204=EB=8B=A8=EA=B3=84]=20?= =?UTF-8?q?=EB=B0=80=EB=A6=AC(=EA=B9=80=EB=AF=B8=EC=84=B1)=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20?= =?UTF-8?q?=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: cookie, session 구현 * refactor: Http Request 파싱 로직 리팩토링 * refactor: Controller 인터페이스 리팩토링 * refactor: HttpResponseGenerator로 응답 메세지 생성하도록 수정 * refactor: FrontController 도입하여 양방향 의존성 제거 * test: cache 학습 테스트 작성 * test: thread 학습 테스트 작성 * feat: Executors로 Thread Pool 적용 * feat: 동시성 컬렉션으로 변경 * refactor: RequestBody 파싱 타입 변경 * docs: 기능 요구사항 작성 * refactor: content-length 헤더 세팅 컨트롤러에서 하도록 수정 * refactor: 코드 가독성 개선 * refactor: static resource 요청을 처리하는 StaticController 구현 * refactor: 세션 책임 분리 * refactor: FrontController를 RequestMapping으로 변경 --- README.md | 19 +++-- .../example/cachecontrol/CacheWebConfig.java | 5 ++ .../example/etag/EtagFilterConfiguration.java | 19 +++-- .../version/CacheBustingWebConfig.java | 7 +- study/src/main/resources/application.yml | 3 + .../thread/stage0/SynchronizationTest.java | 31 ++++---- .../java/thread/stage0/ThreadPoolsTest.java | 30 ++++---- .../src/main/java/handler/RequestHandler.java | 6 -- .../java/handler/RequestHandlerMapping.java | 23 ------ .../src/main/java/nextstep/Application.java | 8 ++- .../java/nextstep/jwp/JwpRequestMapping.java | 37 ++++++++++ .../jwp/controller/IndexController.java | 13 ++-- .../jwp/controller/LoginController.java | 55 +++++++++----- .../jwp/controller/RegisterController.java | 39 +++++----- .../jwp/controller/StaticController.java | 18 +++++ .../java/org/apache/catalina/Manager.java | 36 ++++------ .../java/org/apache/catalina/Session.java | 25 +++++++ .../org/apache/catalina/SessionManager.java | 31 ++++++++ .../apache/catalina/connector/Connector.java | 19 +++-- .../org/apache/catalina/startup/Tomcat.java | 14 ++-- .../coyote/handler/AbstractController.java | 26 +++++++ .../apache/coyote}/handler/Controller.java | 4 +- .../apache/coyote/handler/RequestMapping.java | 6 ++ .../org/apache/coyote/http11/BodyParser.java | 2 +- .../apache/coyote/http11/FormBodyParser.java | 4 +- .../apache/coyote/http11/Http11Processor.java | 71 +++++-------------- .../org/apache/coyote/http11/HttpCookies.java | 19 +++++ .../org/apache/coyote/http11/HttpHeaders.java | 24 ++++--- .../org/apache/coyote/http11/HttpRequest.java | 34 +++++---- .../apache/coyote/http11/HttpRequestLine.java | 51 +++++++++++++ .../coyote/http11/HttpRequestParser.java | 46 ++++++------ .../apache/coyote/http11/HttpResponse.java | 38 +++++++++- .../coyote/http11/HttpResponseGenerator.java | 36 ++++++++++ .../apache/coyote/http11/QueryStrings.java | 11 ++- .../apache/coyote/http11/ViewResolver.java | 11 +-- .../coyote/http11/Http11ProcessorTest.java | 5 +- 36 files changed, 557 insertions(+), 269 deletions(-) delete mode 100644 tomcat/src/main/java/handler/RequestHandler.java delete mode 100644 tomcat/src/main/java/handler/RequestHandlerMapping.java create mode 100644 tomcat/src/main/java/nextstep/jwp/JwpRequestMapping.java create mode 100644 tomcat/src/main/java/nextstep/jwp/controller/StaticController.java create mode 100644 tomcat/src/main/java/org/apache/catalina/Session.java create mode 100644 tomcat/src/main/java/org/apache/catalina/SessionManager.java create mode 100644 tomcat/src/main/java/org/apache/coyote/handler/AbstractController.java rename tomcat/src/main/java/{ => org/apache/coyote}/handler/Controller.java (53%) create mode 100644 tomcat/src/main/java/org/apache/coyote/handler/RequestMapping.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/HttpCookies.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/HttpRequestLine.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/HttpResponseGenerator.java diff --git a/README.md b/README.md index 44611aad18..9aaaa04e88 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,18 @@ - [x] 회원가입 페이지는 GET으로 요청한다. - [x] 회원가입을 완료하면 index.html로 리다이렉트한다. - [x] 로그인 페이지도 버튼을 눌렀을 때 POST 방식으로 전송하도록 변경한다. -- [ ] 서버에서 HTTP 응답을 전달할 때 응답 헤더에 Set-Cookie를 추가한다. - - [ ] Cookie에 JSESSIONID가 없으면 응답 헤더에 Set-Cookie를 반환해준다. -- [ ] 쿠키에서 전달 받은 JSESSIONID의 값으로 로그인 여부를 체크한다. - - [ ] 로그인된 상태에서 /login 페이지에 접근하면 index.html 페이지로 리다이렉트 처리한다. +- [x] 서버에서 HTTP 응답을 전달할 때 응답 헤더에 Set-Cookie를 추가한다. + - [x] Cookie에 JSESSIONID가 없으면 응답 헤더에 Set-Cookie를 반환해준다. +- [x] 쿠키에서 전달 받은 JSESSIONID의 값으로 로그인 여부를 체크한다. + - [x] 로그인된 상태에서 /login 페이지에 접근하면 index.html 페이지로 리다이렉트 처리한다. + +### 3단계 + +- [x] HttpRequest 클래스 구현하기 +- [x] HttpResponse 클래스 구현하기 +- [x] Controller 인터페이스 추가하기 + +### 4단계 + +- [x] Executors로 Thread Pool 적용 +- [x] 동시성 컬렉션 사용하기 diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..79f7c039b0 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,18 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + final WebContentInterceptor interceptor = new WebContentInterceptor(); + interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*"); + registry.addInterceptor(interceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..16a31d5749 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,23 @@ package cache.com.example.etag; +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + final FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns( + "/etag", + PREFIX_STATIC_RESOURCES + "/*" + ); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..d434d70006 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,9 @@ package cache.com.example.version; +import java.time.Duration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -13,13 +15,14 @@ public class CacheBustingWebConfig implements WebMvcConfigurer { private final ResourceVersion version; @Autowired - public CacheBustingWebConfig(ResourceVersion version) { + public CacheBustingWebConfig(final ResourceVersion version) { this.version = version; } @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..385c11d5f1 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -7,3 +7,6 @@ server: max-connections: 1 threads: max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..a8be4931bb 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -1,34 +1,29 @@ package thread.stage0; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; /** - * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. - * 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다. - * 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. - * - * Synchronization - * https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html + * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. 자바는 공유 데이터에 대한 스레드 접근을 + * 동기화(synchronization)하여 경쟁 조건을 방지한다. 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. + *

+ * Synchronization https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html */ class SynchronizationTest { /** - * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. - * synchronized 키워드에 대하여 찾아보고 적용하면 된다. - * - * Guide to the Synchronized Keyword in Java - * https://www.baeldung.com/java-synchronized + * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. synchronized 키워드에 대하여 찾아보고 적용하면 된다. + *

+ * Guide to the Synchronized Keyword in Java https://www.baeldung.com/java-synchronized */ @Test void testSynchronized() throws InterruptedException { - var executorService = Executors.newFixedThreadPool(3); - var synchronizedMethods = new SynchronizedMethods(); + final var executorService = Executors.newFixedThreadPool(3); + final var synchronizedMethods = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> executorService.submit(synchronizedMethods::calculate)); @@ -41,7 +36,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } @@ -49,7 +44,7 @@ public int getSum() { return sum; } - public void setSum(int sum) { + public void setSum(final int sum) { this.sum = sum; } } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..05a7a23136 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -1,23 +1,19 @@ package thread.stage0; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * 스레드 풀은 무엇이고 어떻게 동작할까? - * 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. - * - * Thread Pools - * https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html - * - * Introduction to Thread Pools in Java - * https://www.baeldung.com/thread-pool-java-and-guava + * 스레드 풀은 무엇이고 어떻게 동작할까? 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. + *

+ * Thread Pools https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html + *

+ * Introduction to Thread Pools in Java https://www.baeldung.com/thread-pool-java-and-guava */ class ThreadPoolsTest { @@ -31,8 +27,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +42,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); @@ -57,7 +53,7 @@ private Runnable logWithSleep(final String message) { return () -> { try { Thread.sleep(1000); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { throw new RuntimeException(e); } log.info(message); diff --git a/tomcat/src/main/java/handler/RequestHandler.java b/tomcat/src/main/java/handler/RequestHandler.java deleted file mode 100644 index 758e1f48a3..0000000000 --- a/tomcat/src/main/java/handler/RequestHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package handler; - -public interface RequestHandler { - - Controller getHandler(final String requestUri); -} diff --git a/tomcat/src/main/java/handler/RequestHandlerMapping.java b/tomcat/src/main/java/handler/RequestHandlerMapping.java deleted file mode 100644 index 8bf5931ad8..0000000000 --- a/tomcat/src/main/java/handler/RequestHandlerMapping.java +++ /dev/null @@ -1,23 +0,0 @@ -package handler; - -import java.util.HashMap; -import java.util.Map; -import nextstep.jwp.controller.IndexController; -import nextstep.jwp.controller.LoginController; -import nextstep.jwp.controller.RegisterController; - -public class RequestHandlerMapping implements RequestHandler { - - private final Map handlerMapping = new HashMap<>(); - - public RequestHandlerMapping() { - handlerMapping.put("/", new IndexController()); - handlerMapping.put("/login", new LoginController()); - handlerMapping.put("/register", new RegisterController()); - } - - @Override - public Controller getHandler(final String requestUri) { - return handlerMapping.get(requestUri); - } -} diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index 3dd7593507..2f4baebfd3 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,11 +1,15 @@ package nextstep; +import nextstep.jwp.JwpRequestMapping; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.handler.RequestMapping; public class Application { - public static void main(String[] args) { - final var tomcat = new Tomcat(); + private static final RequestMapping requestMapping = new JwpRequestMapping(); + + public static void main(final String[] args) { + final var tomcat = new Tomcat(requestMapping); tomcat.start(); } } diff --git a/tomcat/src/main/java/nextstep/jwp/JwpRequestMapping.java b/tomcat/src/main/java/nextstep/jwp/JwpRequestMapping.java new file mode 100644 index 0000000000..ee3172152f --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/JwpRequestMapping.java @@ -0,0 +1,37 @@ +package nextstep.jwp; + +import java.util.HashMap; +import java.util.Map; +import nextstep.jwp.controller.IndexController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; +import nextstep.jwp.controller.StaticController; +import org.apache.coyote.handler.Controller; +import org.apache.coyote.handler.RequestMapping; + +public class JwpRequestMapping implements RequestMapping { + + private static final String STATIC_RESOURCE_KEY = "static"; + private static final String EXTENSION_DELIMITER = "."; + + private final Map handlerMapping = new HashMap<>(); + + public JwpRequestMapping() { + handlerMapping.put(STATIC_RESOURCE_KEY, new StaticController()); + handlerMapping.put("/", new IndexController()); + handlerMapping.put("/login", new LoginController()); + handlerMapping.put("/register", new RegisterController()); + } + + @Override + public Controller getHandler(final String requestUri) { + if (isFileRequest(requestUri)) { + return handlerMapping.get(STATIC_RESOURCE_KEY); + } + return handlerMapping.get(requestUri); + } + + private boolean isFileRequest(final String requestUri) { + return requestUri.contains(EXTENSION_DELIMITER); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/IndexController.java b/tomcat/src/main/java/nextstep/jwp/controller/IndexController.java index 7ec0c42496..bafbe91951 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/IndexController.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/IndexController.java @@ -1,15 +1,18 @@ package nextstep.jwp.controller; -import handler.Controller; +import org.apache.coyote.handler.AbstractController; import org.apache.coyote.http11.HttpRequest; import org.apache.coyote.http11.HttpResponse; import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.ViewResolver; -public class IndexController implements Controller { +public class IndexController extends AbstractController { @Override - public String run(final HttpRequest httpRequest, final HttpResponse httpResponse) { - httpResponse.setStatusCode(HttpStatusCode.OK); - return "/index.html"; + public void doGet(final HttpRequest request, final HttpResponse response) { + final String responseBody = ViewResolver.read("/index.html"); + response.setResponseBody(responseBody); + response.setContentLength(); + response.setStatusCode(HttpStatusCode.OK); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java index 451acfbab7..4ae247cb2f 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -1,40 +1,57 @@ package nextstep.jwp.controller; -import handler.Controller; import java.util.Map; +import java.util.Optional; import nextstep.jwp.db.InMemoryUserRepository; -import org.apache.coyote.http11.HttpMethod; +import nextstep.jwp.model.User; +import org.apache.catalina.Session; +import org.apache.catalina.SessionManager; +import org.apache.coyote.handler.AbstractController; import org.apache.coyote.http11.HttpRequest; import org.apache.coyote.http11.HttpResponse; import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.ViewResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LoginController implements Controller { +public class LoginController extends AbstractController { private static final Logger log = LoggerFactory.getLogger(LoginController.class); + private final SessionManager sessionManager = new SessionManager(); + @Override - public String run(final HttpRequest httpRequest, final HttpResponse httpResponse) { - final HttpMethod method = httpRequest.getMethod(); - if (method.equals(HttpMethod.GET)) { - httpResponse.setStatusCode(HttpStatusCode.OK); - return "/login.html"; + public void doGet(final HttpRequest request, final HttpResponse response) { + final String sessionId = request.getCookie(Session.SESSION_KEY); + final Session session = sessionManager.findSession(sessionId); + if (session == null) { + final String responseBody = ViewResolver.read("/login.html"); + response.setResponseBody(responseBody); + response.setStatusCode(HttpStatusCode.OK); + response.setContentLength(); + return; } - final Map requestBody = httpRequest.getRequestBody(); - final String account = requestBody.get("account"); - final String password = requestBody.get("password"); - final boolean isSuccess = InMemoryUserRepository.findByAccount(account) - .filter(user -> user.checkPassword(password)) + response.setStatusCode(HttpStatusCode.FOUND); + response.setLocation("/"); + } + + @Override + public void doPost(final HttpRequest request, final HttpResponse response) { + final Map requestBody = request.getRequestBody(); + final String account = (String) requestBody.get("account"); + final String password = (String) requestBody.get("password"); + final Optional findUser = InMemoryUserRepository.findByAccount(account); + final boolean isSuccess = findUser.filter(user -> user.checkPassword(password)) .isPresent(); if (isSuccess) { - httpResponse.setStatusCode(HttpStatusCode.FOUND); - httpResponse.setHeader("Location", "/"); + final Session session = sessionManager.create(findUser.get()); + response.setCookie(Session.SESSION_KEY, session.getId()); + response.setStatusCode(HttpStatusCode.FOUND); + response.setLocation("/"); log.info("로그인 성공! 로그인 아이디: " + account); - return "/index.html"; + return; } - httpResponse.setStatusCode(HttpStatusCode.FOUND); - httpResponse.setHeader("Location", "/401.html"); - return "/401.html"; + response.setStatusCode(HttpStatusCode.FOUND); + response.setLocation("/401.html"); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java index 55b03f6847..1987a4eca3 100644 --- a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -1,33 +1,32 @@ package nextstep.jwp.controller; -import handler.Controller; import java.util.Map; import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.model.User; -import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.handler.AbstractController; import org.apache.coyote.http11.HttpRequest; import org.apache.coyote.http11.HttpResponse; import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.ViewResolver; -public class RegisterController implements Controller { +public class RegisterController extends AbstractController { @Override - public String run(final HttpRequest httpRequest, final HttpResponse httpResponse) { - final HttpMethod method = httpRequest.getMethod(); - if (method.equals(HttpMethod.GET)) { - httpResponse.setStatusCode(HttpStatusCode.OK); - return "/register.html"; - } - if (method.equals(HttpMethod.POST)) { - final Map requestBody = httpRequest.getRequestBody(); - final String account = requestBody.get("account"); - final String password = requestBody.get("password"); - final String email = requestBody.get("email"); - InMemoryUserRepository.save(new User(account, password, email)); - httpResponse.setStatusCode(HttpStatusCode.FOUND); - httpResponse.setHeader("Location", "/index.html"); - return "/index.html"; - } - return "/index.html"; + public void doGet(final HttpRequest request, final HttpResponse response) { + final String responseBody = ViewResolver.read("/register.html"); + response.setResponseBody(responseBody); + response.setContentLength(); + response.setStatusCode(HttpStatusCode.OK); + } + + @Override + public void doPost(final HttpRequest request, final HttpResponse response) { + final Map requestBody = request.getRequestBody(); + final String account = (String) requestBody.get("account"); + final String password = (String) requestBody.get("password"); + final String email = (String) requestBody.get("email"); + InMemoryUserRepository.save(new User(account, password, email)); + response.setStatusCode(HttpStatusCode.FOUND); + response.setLocation("/index.html"); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/StaticController.java b/tomcat/src/main/java/nextstep/jwp/controller/StaticController.java new file mode 100644 index 0000000000..5a84895820 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/StaticController.java @@ -0,0 +1,18 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.handler.AbstractController; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; +import org.apache.coyote.http11.HttpStatusCode; +import org.apache.coyote.http11.ViewResolver; + +public class StaticController extends AbstractController { + + @Override + public void doGet(final HttpRequest request, final HttpResponse response) { + final String responseBody = ViewResolver.read(request.getRequestUri()); + response.setResponseBody(responseBody); + response.setContentLength(); + response.setStatusCode(HttpStatusCode.OK); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..ff0e8f5f20 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,18 +1,14 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; - import java.io.IOException; /** - * A Manager manages the pool of Sessions that are associated with a - * particular Container. Different Manager implementations may support - * value-added features such as the persistent storage of session data, - * as well as migrating sessions for distributable web applications. + * A Manager manages the pool of Sessions that are associated with a particular Container. Different Manager + * implementations may support value-added features such as the persistent storage of session data, as well as migrating + * sessions for distributable web applications. *

- * In order for a Manager implementation to successfully operate - * with a Context implementation that implements reloading, it - * must obey the following constraints: + * In order for a Manager implementation to successfully operate with a Context implementation + * that implements reloading, it must obey the following constraints: *

    *
  • Must implement Lifecycle so that the Context can indicate * that a restart is required. @@ -29,28 +25,24 @@ public interface Manager { * * @param session Session to be added */ - void add(HttpSession session); + void add(Session session); /** - * Return the active Session, associated with this Manager, with the - * specified session id (if any); otherwise return null. + * Return the active Session, associated with this Manager, with the specified session id (if any); otherwise + * return + * null. * * @param id The session id for the session to be returned - * - * @exception IllegalStateException if a new session cannot be - * instantiated for any reason - * @exception IOException if an input/output error occurs while - * processing this request - * - * @return the request session or {@code null} if a session with the - * requested ID could not be found + * @return the request session or {@code null} if a session with the requested ID could not be found + * @throws IllegalStateException if a new session cannot be instantiated for any reason + * @throws IOException if an input/output error occurs while processing this request */ - HttpSession findSession(String id) throws IOException; + Session findSession(String id) throws IOException; /** * Remove this Session from the active Sessions for this Manager. * * @param session Session to be removed */ - void remove(HttpSession session); + void remove(Session session); } diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java new file mode 100644 index 0000000000..70fa786dac --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Session.java @@ -0,0 +1,25 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + + public static final String SESSION_KEY = "JSESSIONID"; + + private final String id = UUID.randomUUID().toString(); + private final Map values = new HashMap<>(); + + 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); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java new file mode 100644 index 0000000000..8ee3f9ab2b --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -0,0 +1,31 @@ +package org.apache.catalina; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new ConcurrentHashMap<>(); + + public Session create(final Object value) { + final Session session = new Session(); + add(session); + session.setAttribute(session.getId(), value); + return session; + } + + @Override + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(final String id) { + return SESSIONS.getOrDefault(id, null); + } + + @Override + public void remove(final Session session) { + SESSIONS.remove(session.getId()); + } +} 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 6e71945e45..fb61b861f8 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -4,6 +4,9 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.apache.coyote.handler.RequestMapping; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,17 +17,23 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int DEFAULT_MAX_THREAD_COUNT = 250; private final ServerSocket serverSocket; + private final RequestMapping requestMapping; + private final ExecutorService executorService; private boolean stopped; - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + public Connector(final RequestMapping requestMapping) { + this(requestMapping, DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREAD_COUNT); } - public Connector(final int port, final int acceptCount) { + public Connector(final RequestMapping requestMapping, final int port, final int acceptCount, + final int maxThreads) { + this.requestMapping = requestMapping; this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.executorService = Executors.newFixedThreadPool(maxThreads); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -65,8 +74,8 @@ private void process(final Socket connection) { if (connection == null) { return; } - final var processor = new Http11Processor(connection); - new Thread(processor).start(); + final var processor = new Http11Processor(connection, requestMapping); + executorService.execute(processor); } public void stop() { 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 205159e95b..51df95c07b 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,23 +1,29 @@ package org.apache.catalina.startup; +import java.io.IOException; import org.apache.catalina.connector.Connector; +import org.apache.coyote.handler.RequestMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + private final RequestMapping requestMapping; + + public Tomcat(final RequestMapping requestMapping) { + this.requestMapping = requestMapping; + } + public void start() { - var connector = new Connector(); + final var connector = new Connector(requestMapping); connector.start(); try { // make the application wait until we press any key. System.in.read(); - } catch (IOException e) { + } catch (final IOException e) { log.error(e.getMessage(), e); } finally { log.info("web server stop."); diff --git a/tomcat/src/main/java/org/apache/coyote/handler/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/handler/AbstractController.java new file mode 100644 index 0000000000..abd15e6b92 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/AbstractController.java @@ -0,0 +1,26 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(final HttpRequest request, final HttpResponse response) { + final HttpMethod method = request.getMethod(); + + if (method.equals(HttpMethod.GET)) { + doGet(request, response); + } + if (method.equals(HttpMethod.POST)) { + doPost(request, response); + } + } + + protected void doGet(final HttpRequest request, final HttpResponse response) { + } + + protected void doPost(final HttpRequest request, final HttpResponse response) { + } +} diff --git a/tomcat/src/main/java/handler/Controller.java b/tomcat/src/main/java/org/apache/coyote/handler/Controller.java similarity index 53% rename from tomcat/src/main/java/handler/Controller.java rename to tomcat/src/main/java/org/apache/coyote/handler/Controller.java index 366a044f40..7d8d27b0f5 100644 --- a/tomcat/src/main/java/handler/Controller.java +++ b/tomcat/src/main/java/org/apache/coyote/handler/Controller.java @@ -1,9 +1,9 @@ -package handler; +package org.apache.coyote.handler; import org.apache.coyote.http11.HttpRequest; import org.apache.coyote.http11.HttpResponse; public interface Controller { - String run(final HttpRequest httpRequest, final HttpResponse httpResponse); + void service(final HttpRequest request, final HttpResponse response); } diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/handler/RequestMapping.java new file mode 100644 index 0000000000..214b3e9ba9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/RequestMapping.java @@ -0,0 +1,6 @@ +package org.apache.coyote.handler; + +public interface RequestMapping { + + Controller getHandler(final String requestUri); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/BodyParser.java b/tomcat/src/main/java/org/apache/coyote/http11/BodyParser.java index 6ad8bc90dc..064b20f3d1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/BodyParser.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/BodyParser.java @@ -4,5 +4,5 @@ public interface BodyParser { - Map parse(final String body); + Map parse(final String body); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/FormBodyParser.java b/tomcat/src/main/java/org/apache/coyote/http11/FormBodyParser.java index df83c500b9..336447b193 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/FormBodyParser.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/FormBodyParser.java @@ -9,8 +9,8 @@ public class FormBodyParser implements BodyParser { private static final String VALUE_DELIMITER = "="; @Override - public Map parse(final String body) { - final Map parameters = new HashMap<>(); + public Map parse(final String body) { + final Map parameters = new HashMap<>(); final String[] splitParameters = body.split(PARAMETER_DELIMITER); for (final String splitParameter : splitParameters) { final String[] parameter = splitParameter.split(VALUE_DELIMITER); 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 ddc4730fad..0470cf46ca 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,32 +1,27 @@ package org.apache.coyote.http11; -import static java.util.stream.Collectors.joining; - -import handler.Controller; -import handler.RequestHandler; -import handler.RequestHandlerMapping; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; -import java.util.Map; import org.apache.coyote.Processor; +import org.apache.coyote.handler.Controller; +import org.apache.coyote.handler.RequestMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Http11Processor implements Runnable, Processor { - private static final String EXTENSION_DELIMITER = "."; - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private static final HttpRequestParser httpRequestParser = new HttpRequestParser(); - private static final RequestHandler requestHandler = new RequestHandlerMapping(); - private static final ViewResolver viewResolver = new ViewResolver(); + private static final HttpResponseGenerator httpResponseGenerator = new HttpResponseGenerator(); private final Socket connection; + private final RequestMapping requestMapping; - public Http11Processor(final Socket connection) { + public Http11Processor(final Socket connection, final RequestMapping requestMapping) { this.connection = connection; + this.requestMapping = requestMapping; } @Override @@ -37,18 +32,18 @@ public void run() { @Override public void process(final Socket connection) { - try (final var bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - final var outputStream = connection.getOutputStream()) { + try (final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream(); + final var bufferedReader = new BufferedReader(new InputStreamReader(inputStream)) + ) { final HttpRequest httpRequest = httpRequestParser.parse(bufferedReader); final HttpResponse httpResponse = new HttpResponse(); - final String viewName = getViewName(httpRequest, httpResponse); - final String responseBody = viewResolver.read(viewName); + doService(httpRequest, httpResponse); - httpResponse.setHeader("Content-Type", httpRequest.getContentType()); - httpResponse.setHeader("Content-Length", String.valueOf(responseBody.getBytes().length)); - httpResponse.setResponseBody(responseBody); - final String response = makeResponseString(httpResponse); + httpResponse.setProtocol(httpRequest.getProtocol()); + httpResponse.setHeader(HttpHeaders.CONTENT_TYPE, httpRequest.getContentType()); + final String response = httpResponseGenerator.generate(httpResponse); outputStream.write(response.getBytes()); outputStream.flush(); @@ -57,40 +52,10 @@ public void process(final Socket connection) { } } - private String getViewName(final HttpRequest httpRequest, final HttpResponse httpResponse) { - final String requestUri = httpRequest.getRequestUri(); - - if (isFileRequest(requestUri)) { - httpResponse.setStatusCode(HttpStatusCode.OK); - return requestUri; - } - final Controller controller = requestHandler.getHandler(requestUri); - return controller.run(httpRequest, httpResponse); - } - - private boolean isFileRequest(final String requestUri) { - return requestUri.contains(EXTENSION_DELIMITER); - } - - private String makeResponseString(final HttpResponse httpResponse) { - return String.join(System.lineSeparator(), - makeResponseCode(httpResponse), - makeResponseHeaders(httpResponse), - "", - httpResponse.getResponseBody()); - } - - private String makeResponseCode(final HttpResponse httpResponse) { - final int code = httpResponse.getStatusCode().getCode(); - final String message = httpResponse.getStatusCode().getMessage(); - return "HTTP/1.1 " + code + " " + message + " "; - } + private void doService(final HttpRequest request, final HttpResponse response) { + final String requestUri = request.getRequestUri(); - private String makeResponseHeaders(final HttpResponse httpResponse) { - final Map headers = httpResponse.getHeaders(); - return headers.entrySet() - .stream() - .map(entry -> entry.getKey() + ": " + entry.getValue() + " ") - .collect(joining(System.lineSeparator())); + final Controller controller = requestMapping.getHandler(requestUri); + controller.service(request, response); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookies.java new file mode 100644 index 0000000000..e1b75b183e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookies.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; + +public class HttpCookies { + + private static final String EMPTY_VALUE = ""; + + private final Map values = new HashMap<>(); + + public void add(final String key, final String value) { + values.put(key, value); + } + + public String get(final String key) { + return values.getOrDefault(key, EMPTY_VALUE); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java index 74f200c3d6..7e1d085c7b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java @@ -5,31 +5,35 @@ public class HttpHeaders { + public static final String COOKIE = "Cookie"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String ACCEPT = "Accept"; + public static final String LOCATION = "Location"; + public static final String SET_COOKIE = "Set-Cookie"; + private static final String EMPTY_VALUE = ""; private static final String CONTENT_TYPE_DELIMITER = ","; - private final Map headers = new HashMap<>(); - - public HttpHeaders() { - } + private final Map values = new HashMap<>(); public void add(final String key, final String value) { - headers.put(key, value); + values.put(key, value); } public String get(final String headerName) { - return headers.getOrDefault(headerName, EMPTY_VALUE); + return values.getOrDefault(headerName, EMPTY_VALUE); } public String getContentType() { - String value = headers.getOrDefault("Content-Type", EMPTY_VALUE); + String value = values.getOrDefault(CONTENT_TYPE, EMPTY_VALUE); if (value.isEmpty()) { - value = headers.get("Accept"); + value = values.get(ACCEPT); } return value.split(CONTENT_TYPE_DELIMITER)[0]; } - public Map getHeaders() { - return headers; + public Map getValues() { + return values; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java index 29da5adfab..1acf6db1f4 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java @@ -6,39 +6,47 @@ public class HttpRequest { private static final String EMPTY_VALUE = ""; - private final HttpMethod method; - private final String requestUri; + private final HttpRequestLine requestLine; private final QueryStrings queryStrings; private final HttpHeaders headers; - private final Map requestBody; + private final HttpCookies cookies; + private final Map requestBody; - public HttpRequest(final HttpMethod method, final String requestUri, final QueryStrings queryStrings, - final HttpHeaders headers, final Map requestBody) { - this.method = method; - this.requestUri = requestUri; + public HttpRequest(final HttpRequestLine requestLine, final QueryStrings queryStrings, final HttpHeaders headers, + final HttpCookies cookies, final Map requestBody) { + this.requestLine = requestLine; this.queryStrings = queryStrings; this.headers = headers; + this.cookies = cookies; this.requestBody = requestBody; } public HttpMethod getMethod() { - return method; + return requestLine.getMethod(); } public String getRequestUri() { - return requestUri; + return requestLine.getRequestUri(); + } + + public String getProtocol() { + return requestLine.getProtocol(); } public String getContentType() { return headers.getContentType(); } - public String getParameter(final String key) { - final Map parameters = queryStrings.getQueryStrings(); - return parameters.getOrDefault(key, EMPTY_VALUE); + public String getQueryString(final String key) { + final Map values = queryStrings.getValues(); + return values.getOrDefault(key, EMPTY_VALUE); } - public Map getRequestBody() { + public Map getRequestBody() { return requestBody; } + + public String getCookie(final String key) { + return cookies.get(key); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestLine.java new file mode 100644 index 0000000000..61638d2fc2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestLine.java @@ -0,0 +1,51 @@ +package org.apache.coyote.http11; + +public class HttpRequestLine { + + private static final String REQUEST_LINE_DELIMITER = " "; + private static final int HTTP_METHOD_INDEX = 0; + private static final int REQUEST_URI_INDEX = 1; + private static final int PROTOCOL_INDEX = 2; + private static final String QUERY_STRING_DELIMITER = "?"; + private static final int NON_EXIST = -1; + + private final HttpMethod method; + private final String requestUri; + private final String protocol; + + public HttpRequestLine(final String requestLine) { + final String[] requestLineValues = requestLine.split(REQUEST_LINE_DELIMITER); + this.method = parseHttpMethod(requestLineValues); + this.requestUri = parseRequestUri(requestLineValues); + this.protocol = parseProtocol(requestLineValues); + } + + private HttpMethod parseHttpMethod(final String[] values) { + return HttpMethod.valueOf(values[HTTP_METHOD_INDEX]); + } + + private String parseRequestUri(final String[] values) { + final String path = values[REQUEST_URI_INDEX]; + final int queryStringBeginIndex = path.indexOf(QUERY_STRING_DELIMITER); + if (queryStringBeginIndex == NON_EXIST) { + return path; + } + return path.substring(0, queryStringBeginIndex); + } + + private String parseProtocol(final String[] values) { + return values[PROTOCOL_INDEX]; + } + + public HttpMethod getMethod() { + return method; + } + + public String getRequestUri() { + return requestUri; + } + + public String getProtocol() { + return protocol; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java index a728441618..fe42fbfe9d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java @@ -9,7 +9,6 @@ public class HttpRequestParser { private static final String REQUEST_LINE_DELIMITER = " "; - private static final int HTTP_METHOD_INDEX = 0; private static final int REQUEST_URI_INDEX = 1; private static final String QUERY_STRING_DELIMITER = "?"; private static final int NON_EXIST = -1; @@ -18,6 +17,7 @@ public class HttpRequestParser { private static final String EMPTY_LINE = ""; private static final String HEADER_DELIMITER = ": "; private static final int EXIST_HEADER_VALUE = 2; + private static final String COOKIE_DELIMITER = "; "; private static final int KEY_INDEX = 0; private static final int VALUE_INDEX = 1; @@ -32,15 +32,17 @@ private void init() { } public HttpRequest parse(final BufferedReader reader) { - final String firstLine = readLine(reader); - final HttpMethod httpMethod = parseHttpMethod(firstLine); - final String requestUri = parseRequestUri(firstLine); - final QueryStrings queryStrings = parseQueryStrings(firstLine); + final String requestLine = readLine(reader); + final HttpRequestLine httpRequestLine = new HttpRequestLine(requestLine); + final QueryStrings queryStrings = parseQueryStrings(requestLine); + final HttpHeaders httpHeaders = parseHttpHeaders(reader); - final Map requestBody = parseRequestBody(reader, httpHeaders.get("Content-Length"), + final HttpCookies httpCookies = parseHttpCookies(httpHeaders.get(HttpHeaders.COOKIE)); + + final Map requestBody = parseRequestBody(reader, httpHeaders.get(HttpHeaders.CONTENT_LENGTH), bodyParsers.get(httpHeaders.getContentType())); - return new HttpRequest(httpMethod, requestUri, queryStrings, httpHeaders, requestBody); + return new HttpRequest(httpRequestLine, queryStrings, httpHeaders, httpCookies, requestBody); } private String readLine(final BufferedReader reader) { @@ -51,19 +53,6 @@ private String readLine(final BufferedReader reader) { } } - private HttpMethod parseHttpMethod(final String line) { - return HttpMethod.valueOf(line.split(REQUEST_LINE_DELIMITER)[HTTP_METHOD_INDEX]); - } - - private String parseRequestUri(final String line) { - final String requestUri = line.split(REQUEST_LINE_DELIMITER)[REQUEST_URI_INDEX]; - final int queryStringBeginIndex = requestUri.indexOf(QUERY_STRING_DELIMITER); - if (queryStringBeginIndex == NON_EXIST) { - return requestUri; - } - return requestUri.substring(0, queryStringBeginIndex); - } - private QueryStrings parseQueryStrings(final String line) { final QueryStrings queryStrings = new QueryStrings(); final String requestUri = line.split(REQUEST_LINE_DELIMITER)[REQUEST_URI_INDEX]; @@ -97,7 +86,22 @@ private HttpHeaders parseHttpHeaders(final BufferedReader reader) { return httpHeaders; } - private Map parseRequestBody(final BufferedReader reader, final String contentLength, + private HttpCookies parseHttpCookies(final String cookieValue) { + final HttpCookies httpCookies = new HttpCookies(); + if (cookieValue.isEmpty()) { + return httpCookies; + } + final String[] cookies = cookieValue.split(COOKIE_DELIMITER); + for (final String cookie : cookies) { + final String[] splitKeyValue = cookie.split(KEY_AND_VALUE_DELIMITER); + final String key = splitKeyValue[KEY_INDEX]; + final String value = splitKeyValue[VALUE_INDEX]; + httpCookies.add(key, value); + } + return httpCookies; + } + + private Map parseRequestBody(final BufferedReader reader, final String contentLength, final BodyParser bodyParser) { if (contentLength.isEmpty()) { return new HashMap<>(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java index 00dcadd9e0..f9f5602fce 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -4,16 +4,21 @@ public class HttpResponse { + private static final String COOKIE_KEY_VALUE_DELIMITER = "="; + private HttpStatusCode statusCode; + private String protocol; private final HttpHeaders headers; private String responseBody; public HttpResponse() { - this(null, new HttpHeaders(), null); + this(null, null, new HttpHeaders(), null); } - public HttpResponse(final HttpStatusCode statusCode, final HttpHeaders headers, final String responseBody) { + public HttpResponse(final HttpStatusCode statusCode, final String protocol, final HttpHeaders headers, + final String responseBody) { this.statusCode = statusCode; + this.protocol = protocol; this.headers = headers; this.responseBody = responseBody; } @@ -27,7 +32,7 @@ public void setStatusCode(final HttpStatusCode statusCode) { } public Map getHeaders() { - return headers.getHeaders(); + return headers.getValues(); } public void setHeader(final String key, final String value) { @@ -41,4 +46,31 @@ public String getResponseBody() { public void setResponseBody(final String responseBody) { this.responseBody = responseBody; } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(final String protocol) { + this.protocol = protocol; + } + + public int getContentLength() { + if (responseBody == null) { + return 0; + } + return responseBody.getBytes().length; + } + + public void setLocation(final String path) { + headers.add(HttpHeaders.LOCATION, path); + } + + public void setCookie(final String key, final String value) { + headers.add(HttpHeaders.SET_COOKIE, String.join(COOKIE_KEY_VALUE_DELIMITER, key, value)); + } + + public void setContentLength() { + headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(getContentLength())); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseGenerator.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseGenerator.java new file mode 100644 index 0000000000..46eb95d61f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseGenerator.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11; + +import static java.util.stream.Collectors.joining; + +import java.util.Map; + +public class HttpResponseGenerator { + + private static final String EMPTY_VALUE = ""; + private static final String RESPONSE_LINE_DELIMITER = " "; + private static final String HEADER_DELIMITER = ": "; + private static final String END_SPACE = " "; + + public String generate(final HttpResponse response) { + return String.join(System.lineSeparator(), + makeResponseCode(response), + makeResponseHeaders(response), + EMPTY_VALUE, + response.getResponseBody()); + } + + private String makeResponseCode(final HttpResponse response) { + final String protocol = response.getProtocol(); + final int code = response.getStatusCode().getCode(); + final String message = response.getStatusCode().getMessage(); + return String.join(RESPONSE_LINE_DELIMITER, protocol, String.valueOf(code), message) + END_SPACE; + } + + private String makeResponseHeaders(final HttpResponse response) { + final Map headers = response.getHeaders(); + return headers.entrySet() + .stream() + .map(entry -> entry.getKey() + HEADER_DELIMITER + entry.getValue() + END_SPACE) + .collect(joining(System.lineSeparator())); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java b/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java index 10d24491bc..9faf2cb518 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/QueryStrings.java @@ -5,16 +5,13 @@ public class QueryStrings { - private final Map queryStrings = new HashMap<>(); - - public QueryStrings() { - } + private final Map values = new HashMap<>(); public void add(final String key, final String value) { - queryStrings.put(key, value); + values.put(key, value); } - public Map getQueryStrings() { - return queryStrings; + public Map getValues() { + return values; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java index 2555307889..9b80ee5ba6 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java @@ -10,9 +10,12 @@ public class ViewResolver { - private final ClassLoader classLoader = getClass().getClassLoader(); + private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - public String read(final String fileResource) { + private ViewResolver() { + } + + public static String read(final String fileResource) { final URL resource = classLoader.getResource("static" + fileResource); final String resourceFile = getFile(resource); final Path path = Paths.get(resourceFile); @@ -20,14 +23,14 @@ public String read(final String fileResource) { return String.join(System.lineSeparator(), fileLines); } - private String getFile(final URL resource) { + private static String getFile(final URL resource) { if (resource == null) { throw new IllegalArgumentException("경로가 올바르지 않습니다."); } return resource.getFile(); } - private List readFileLines(final Path path) { + private static List readFileLines(final Path path) { try { return Files.readAllLines(path); } catch (final IOException e) { diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 000e9a6e64..7d67ad0c4e 100644 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import nextstep.jwp.JwpRequestMapping; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; import support.StubSocket; @@ -18,7 +19,7 @@ class Http11ProcessorTest { void 루트경로로_요청을_보내면_index_페이지를_응답한다() throws IOException { // given final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final var processor = new Http11Processor(socket, new JwpRequestMapping()); // when processor.process(socket); @@ -53,7 +54,7 @@ class Http11ProcessorTest { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(socket, new JwpRequestMapping()); // when processor.process(socket);