diff --git a/README.md b/README.md index 09062ee09f..25bd7e597f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,22 @@ - [x] Request, Response 객체 분리 - [x] Http11Processor의 메서드 분리 -- [ ] Controller 분리 +- [x] Controller 분리 +- [x] 테스트 작성하기 + - [x] / 페이지 리다이렉트 확인 + - [x] /index.html 페이지 리다이렉트 확인 + - [x] /login 페이지 리다이렉트 확인 + - [x] 로그인 성공 시 index.html 리다이렉트 및 set-cookie 설정 여부 확인 + - [x] 로그인 실패 시 401.html 파일 리다이렉트 확인 + - [x] 로그인 후 /login 페이지 접근 시 index.html 리다이렉트 확인 + - [x] /register 페이지 리다이렉트 확인 + - [x] 회원가입 시 로그인 가능해지는지 확인 + - [x] 회원가입 후 index.html 리다이렉트 확인 + - [x] 404 페이지 리다이렉트 확인 + - [x] 유효성 검증 테스트 -## 4단계 - 동시성 확장하기 \ No newline at end of file +## 4단계 - 동시성 확장하기 + +- [x] 학습테스트 마무리 +- [x] Connector의 동작을 ThreadPool에서 관리하도록 구현 +- [x] 세션을 저장하는 객체를 Concurrent Collection으로 변 \ No newline at end of file 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..3ad2ff7783 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,27 @@ package cache.com.example.cachecontrol; +import cache.com.example.version.ResourceVersion; 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; + +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; @Configuration public class CacheWebConfig implements WebMvcConfigurer { + private final ResourceVersion version; + + public CacheWebConfig(final ResourceVersion version) { + this.version = version; + } @Override public void addInterceptors(final InterceptorRegistry registry) { + WebContentInterceptor interceptor = new WebContentInterceptor(); + interceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/*"); + registry.addInterceptor(interceptor) + .excludePathPatterns(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**"); } } 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..08454f435c 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,27 @@ package cache.com.example.etag; +import cache.com.example.version.ResourceVersion; +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; + +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + private final ResourceVersion version; + + public EtagFilterConfiguration(final ResourceVersion version) { + this.version = version; + } + + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + final FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ShallowEtagHeaderFilter()); + registration.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/*"); + return registration; + } } 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..5a422e5b2e 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,9 +2,12 @@ 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; +import java.time.Duration; + @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { @@ -20,6 +23,7 @@ public CacheBustingWebConfig(ResourceVersion 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..7afeb4b2aa 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 \ No newline at end of file diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..cd4ebf0c63 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -12,7 +12,7 @@ * 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다. * 자바는 공유 데이터에 대한 스레드 접근을 동기화(synchronization)하여 경쟁 조건을 방지한다. * 동기화된 블록은 하나의 스레드만 접근하여 실행할 수 있다. - * + *

* Synchronization * https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html */ @@ -21,7 +21,7 @@ class SynchronizationTest { /** * 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자. * synchronized 키워드에 대하여 찾아보고 적용하면 된다. - * + *

* Guide to the Synchronized Keyword in Java * https://www.baeldung.com/java-synchronized */ @@ -41,7 +41,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + synchronized public 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..0aab68a43b 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -12,10 +12,10 @@ /** * 스레드 풀은 무엇이고 어떻게 동작할까? * 테스트를 통과시키고 왜 해당 결과가 나왔는지 생각해보자. - * + *

* 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 */ @@ -31,8 +31,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 +46,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()); diff --git a/tomcat/build.gradle b/tomcat/build.gradle index 5e2a76a777..df54f22041 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -20,6 +20,7 @@ dependencies { testImplementation "org.assertj:assertj-core:3.24.2" testImplementation "org.mockito:mockito-core:5.4.0" + testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.0" testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/DefaultGetController.java b/tomcat/src/main/java/nextstep/jwp/controller/DefaultGetController.java new file mode 100644 index 0000000000..6862c72d49 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/DefaultGetController.java @@ -0,0 +1,27 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.controller.AbstractController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +public class DefaultGetController extends AbstractController { + + + @Override + public boolean isSupported(HttpRequest request) { + return request.isGET() && request.isSamePath("/"); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) { + String responseBody = "Hello world!"; + + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginGetController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginGetController.java new file mode 100644 index 0000000000..38ea1fc83d --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginGetController.java @@ -0,0 +1,51 @@ +package nextstep.jwp.controller; + +import org.apache.catalina.SessionManager; +import org.apache.coyote.http11.controller.AbstractController; +import org.apache.coyote.http11.request.HttpCookie; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; + +public class LoginGetController extends AbstractController { + public static final String JSESSIONID = "JSESSIONID"; + + @Override + public boolean isSupported(HttpRequest request) { + return request.isGET() && request.isSamePath("/login"); + } + + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + final HttpCookie cookie = request.getCookie(); + URL filePathUrl; + if (isLogin(cookie)) { + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(0)) + .addLocation("/index.html") + .build(); + response.updateResponse(HttpResponseStatus.FOUND, responseHeader, ""); + return; + } + filePathUrl = getClass().getResource("/static/login.html"); + String responseBody = readHtmlFile(filePathUrl); + + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody); + } + + private boolean isLogin(HttpCookie cookie) { + return cookie.isExist(JSESSIONID) + && SessionManager.getInstance().findSession(cookie.findCookie(JSESSIONID)) != null; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginPostController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginPostController.java new file mode 100644 index 0000000000..7aeaf8a534 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginPostController.java @@ -0,0 +1,60 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.SessionManager; +import org.apache.coyote.http11.controller.AbstractController; +import org.apache.coyote.http11.exception.UnauthorizeException; +import org.apache.coyote.http11.request.HttpCookie; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.util.Map; +import java.util.UUID; + +public class LoginPostController extends AbstractController { + public static final String JSESSIONID = "JSESSIONID"; + + @Override + public boolean isSupported(HttpRequest request) { + return request.isPOST() && request.isSamePath("/login"); + } + + @Override + protected void doPost(HttpRequest request, HttpResponse response) { + if (request.isNotExistBody()) { + throw new IllegalArgumentException("로그인 정보가 입력되지 않았습니다."); + } + final HttpCookie cookie = request.getCookie(); + Map parsedRequestBody = request.parseBody(); + User user = InMemoryUserRepository.findByAccount(parsedRequestBody.get("account")) + .orElseThrow(() -> new UnauthorizeException("입력한 회원 ID가 존재하지 않습니다.")); + if (isLoginFail(user, parsedRequestBody)) { + throw new UnauthorizeException("입력한 회원 ID가 존재하지 않습니다."); + } + if (!cookie.isExist(JSESSIONID)) { + String jSessionId = String.valueOf(UUID.randomUUID()); + String setCookie = JSESSIONID + "=" + jSessionId; + SessionManager.getInstance().addLoginSession(jSessionId, user); + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), String.valueOf(0)) + .addLocation("/index.html") + .addSetCookie(setCookie) + .build(); + response.updateResponse(HttpResponseStatus.FOUND, responseHeader, ""); + return; + } + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), String.valueOf(0)) + .addLocation("/index.html") + .build(); + response.updateResponse(HttpResponseStatus.FOUND, responseHeader, ""); + + } + + private boolean isLoginFail(User user, Map parsedRequestBody) { + return !user.checkPassword(parsedRequestBody.get("password")); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterGetController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterGetController.java new file mode 100644 index 0000000000..fd655142f3 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterGetController.java @@ -0,0 +1,32 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.controller.AbstractController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; + +public class RegisterGetController extends AbstractController { + + + @Override + public boolean isSupported(HttpRequest request) { + return request.isGET() && request.isSamePath("/register"); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + URL filePathUrl = getClass().getResource("/static/register.html"); + String responseBody = readHtmlFile(filePathUrl); + + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterPostController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterPostController.java new file mode 100644 index 0000000000..fd84e80399 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterPostController.java @@ -0,0 +1,40 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.controller.AbstractController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.util.Map; + +public class RegisterPostController extends AbstractController { + + @Override + public boolean isSupported(HttpRequest request) { + return request.isPOST() && request.isSamePath("/register"); + } + + + @Override + protected void doPost(HttpRequest request, HttpResponse response) { + if (request.isNotExistBody()) { + throw new IllegalArgumentException("회원가입 정보가 입력되지 않았습니다."); + } + Map parsedRequestBody = request.parseBody(); + InMemoryUserRepository.save(new User( + Long.getLong(parsedRequestBody.get("id")), + parsedRequestBody.get("account"), + parsedRequestBody.get("password"), + parsedRequestBody.get("email") + )); + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(0)) + .addLocation("/index.html") + .build(); + response.updateResponse(HttpResponseStatus.FOUND, responseHeader, ""); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index 8ccc4d3140..a44d7b314d 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -21,31 +21,31 @@ */ public interface Manager { - /** - * Add this Session to the set of active Sessions for this Manager. - * - * @param session Session to be added - */ - void add(Session session); + /** + * Add this Session to the set of active Sessions for this Manager. + * + * @param session Session to be added + */ + void add(Session session); - /** - * 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 - * @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 - */ - Session findSession(String id) throws IOException; + /** + * 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 + * @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 + */ + Session findSession(String id) throws IOException; + + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + */ + void remove(Session session); - /** - * Remove this Session from the active Sessions for this Manager. - * - * @param session Session to be removed - */ - void remove(Session session); - - void remove(String id); + void remove(String id); } diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java index f3f1d9f7f5..22270c0b06 100644 --- a/tomcat/src/main/java/org/apache/catalina/Session.java +++ b/tomcat/src/main/java/org/apache/catalina/Session.java @@ -1,34 +1,34 @@ package org.apache.catalina; -import java.util.HashMap; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; public class Session { - private final String id; - private final Map values = new HashMap<>(); + private final String id; + private final ConcurrentMap values = new ConcurrentHashMap<>(); - public Session(final String id) { - this.id = id; - } + public Session(final String id) { + this.id = id; + } + + public String getId() { + return this.id; + } - public String getId() { - return this.id; - } + public Object getAttribute(final String name) { + return values.get(name); + } - public Object getAttribute(final String name) { - return values.get(name); - } + public void setAttribute(final String name, final Object value) { + this.values.put(name, value); + } - public void setAttribute(final String name, final Object value) { - this.values.put(name, value); - } + public void removeAttribute(final String name) { + this.values.remove(name); + } - public void removeAttribute(final String name) { - this.values.remove(name); - } - - public void invalidate() { - this.values.clear(); - } + public void invalidate() { + this.values.clear(); + } } diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java index 9f3d53c3c9..71ea48511a 100644 --- a/tomcat/src/main/java/org/apache/catalina/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -1,46 +1,45 @@ package org.apache.catalina; -import java.util.HashMap; -import java.util.Map; import nextstep.jwp.model.User; -public class SessionManager implements Manager { - - private static final SessionManager INSTANCE = new SessionManager(); - private static final Map SESSIONS = new HashMap<>(); - - public static SessionManager InstanceOf() { - return INSTANCE; - } - - @Override - public void add(final Session session) { - SESSIONS.put(session.getId(), session); - } - - public void addLoginSession(final String jSessionId, final User user) { - Session session = new Session(jSessionId); - session.setAttribute("user", user); - INSTANCE.add(session); //세션 매니저에 세션을 추가한다. - } +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; - @Override - public Session findSession(final String id) { - return SESSIONS.get(id); - } - - @Override - public void remove(Session session) { - SESSIONS.remove(session.getId(), session); - } - - - @Override - public void remove(final String id) { - SESSIONS.remove(id); - } - - private SessionManager() { - } +public class SessionManager implements Manager { + private static final SessionManager instance = new SessionManager(); + private static final ConcurrentMap SESSIONS = new ConcurrentHashMap<>(); + + private SessionManager() { + } + + public static SessionManager getInstance() { + return instance; + } + + @Override + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public void addLoginSession(final String jSessionId, final User user) { + Session session = new Session(jSessionId); + session.setAttribute("user", user); + instance.add(session); + } + + @Override + public Session findSession(final String id) { + return SESSIONS.getOrDefault(id, null); + } + + @Override + public void remove(Session session) { + SESSIONS.remove(session.getId(), session); + } + + @Override + public void remove(final String id) { + SESSIONS.remove(id); + } } diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..d5be1bc80c 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -8,6 +8,7 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.*; public class Connector implements Runnable { @@ -15,17 +16,21 @@ 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 = 250; private final ServerSocket serverSocket; private boolean stopped; + private final ExecutorService threadPool; public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREAD); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, final int maxThreads) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.threadPool = Executors.newCachedThreadPool(); + new ThreadPoolExecutor(acceptCount, maxThreads, 300L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100)); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -67,7 +72,7 @@ private void process(final Socket connection) { return; } var processor = new Http11Processor(connection); - new Thread(processor).start(); + threadPool.submit(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 90742ddef4..322ab94a78 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,27 +1,28 @@ package org.apache.catalina.startup; -import java.io.IOException; import org.apache.catalina.connector.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class Tomcat { +import java.io.IOException; - private static final Logger log = LoggerFactory.getLogger(Tomcat.class); +public class Tomcat { - public void start() { - var connector = new Connector(); - connector.start(); + private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + + public void start() { + var connector = new Connector(); + connector.start(); - try { - // make the application wait until we press any key. - System.in.read(); + try { + // make the application wait until we press any key. + System.in.read(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } finally { - log.info("web server stop."); - connector.stop(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + log.info("web server stop."); + connector.stop(); + } } - } } 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 fbbfb5f986..5ca58b459e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,272 +1,48 @@ package org.apache.coyote.http11; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.Socket; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; -import nextstep.jwp.db.InMemoryUserRepository; -import nextstep.jwp.exception.UncheckedServletException; -import nextstep.jwp.model.User; -import org.apache.catalina.SessionManager; import org.apache.coyote.Processor; +import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class Http11Processor implements Runnable, Processor { - - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - public static final String JSESSIONID = "JSESSIONID"; - - private final Socket connection; - - public Http11Processor(final Socket connection) { - this.connection = connection; - } - - @Override - public void run() { - log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); - process(connection); - } - - @Override - public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - final HttpRequest httpRequest = parseHttpRequest(bufferedReader); - if (httpRequest == null) { - return; - } - final HttpResponse response = handleRequest(httpRequest); - - outputStream.write(response.toString().getBytes()); - outputStream.flush(); - } catch (IOException | UncheckedServletException | URISyntaxException e) { - log.error(e.getMessage(), e); - } - } - - private HttpRequest parseHttpRequest(BufferedReader bufferedReader) throws IOException { - final String startLine = extractStartLine(bufferedReader); - if (startLine == null) { - return null; - } - final Map requestHeaders = extractHeader(bufferedReader); - final String requestBody = extractBody(requestHeaders.get("Content-Length"), bufferedReader); - return new HttpRequest(startLine, requestHeaders, requestBody); - } - - private String extractStartLine(final BufferedReader bufferedReader) throws IOException { - return bufferedReader.readLine(); - } - - private Map extractHeader(final BufferedReader bufferedReader) - throws IOException { - Map headers = new HashMap<>(); - String line = bufferedReader.readLine(); - while (!"".equals(line)) { - String[] tokens = line.split(": "); - headers.put(tokens[0], tokens[1]); - line = bufferedReader.readLine(); - } - return headers; - } - - private String extractBody(String contentLength, BufferedReader bufferedReader) - throws IOException { - if (contentLength == null) { - return null; - } - int length = Integer.parseInt(contentLength); - char[] buffer = new char[length]; - bufferedReader.read(buffer, 0, length); - return new String(buffer); - } - - private HttpResponse handleRequest(final HttpRequest request) - throws URISyntaxException, IOException { - try { - if (request.isPOST() && request.isSamePath("/register")) { - return handlePostRegister(request); - } - if (request.isPOST() && request.isSamePath("/login")) { - return handlePostLogin(request); - } - if (request.isGET() && request.isSamePath("/register")) { - return handleGetRegister(request); - } - if (request.isGET() && request.isSamePath("/login")) { - return handleGetLogin(request); - } - if (request.isGET() && request.isSamePath("/")) { - String responseBody = "Hello world!"; - - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.OK, responseHeader, responseBody); - } - return handleDefault(request).orElse(handle404()); - } catch (IllegalArgumentException exception) { - return handle500(); - } - } - - private HttpResponse handlePostRegister(final HttpRequest request) { - if (request.isNotExistBody()) { - throw new IllegalArgumentException("로그인 정보가 입력되지 않았습니다."); - } - Map parsedRequestBody = parseRequestBody(request); - InMemoryUserRepository.save(new User( - Long.getLong(parsedRequestBody.get("id")), - parsedRequestBody.get("account"), - parsedRequestBody.get("password"), - parsedRequestBody.get("email") - )); - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(0), "/index.html", null); - return HttpResponse.of(HttpResponseStatus.FOUND, responseHeader, null); - } - - private HttpResponse handlePostLogin(final HttpRequest request) - throws URISyntaxException, IOException { - if (request.isNotExistBody()) { - throw new IllegalArgumentException("로그인 정보가 입력되지 않았습니다."); - } - final HttpCookie cookie = request.getCookie(); - Map parsedRequestBody = parseRequestBody(request); - Optional userOptional = InMemoryUserRepository.findByAccount( - parsedRequestBody.get("account")); - if (userOptional.isPresent() - && userOptional.get().checkPassword(parsedRequestBody.get("password"))) { - String setCookie = null; - if (!cookie.isExist(JSESSIONID)) { - String jSessionId = String.valueOf(UUID.randomUUID()); - setCookie = JSESSIONID + "=" + jSessionId; - SessionManager.InstanceOf().addLoginSession(jSessionId, userOptional.get()); - } - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(0), "/index.html", setCookie); - return HttpResponse.of(HttpResponseStatus.FOUND, responseHeader, null); - } - return handle401(request); - } - - private HttpResponse handle401(HttpRequest request) throws URISyntaxException, IOException { - String responseBody = getHtmlFile(getClass().getResource("/static/401.html")); - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.UNAUTHORIZATION, responseHeader, responseBody); - } - - private Map parseRequestBody(HttpRequest request) { - Map parsedRequestBody = new HashMap<>(); - String[] queryTokens = request.getBody().split("&"); - for (String queryToken : queryTokens) { - int equalSeparatorIndex = queryToken.indexOf("="); - if (equalSeparatorIndex != -1) { - parsedRequestBody.put(queryToken.substring(0, equalSeparatorIndex), - queryToken.substring(equalSeparatorIndex + 1)); - - } - } - return parsedRequestBody; - } - - private HttpResponse handleGetRegister(final HttpRequest request) - throws URISyntaxException, IOException { +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.Socket; - URL filePathUrl = getClass().getResource("/static/register.html"); - String responseBody = getHtmlFile(filePathUrl); +public class Http11Processor implements Runnable, Processor { - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.OK, responseHeader, responseBody); - } + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private final Socket connection; - private HttpResponse handleGetLogin(final HttpRequest request) - throws URISyntaxException, IOException { - final HttpCookie cookie = request.getCookie(); - URL filePathUrl; - if (isLogin(cookie)) { - filePathUrl = getClass().getResource("/static/index.html"); - } else { - filePathUrl = getClass().getResource("/static/login.html"); + public Http11Processor(final Socket connection) { + this.connection = connection; } - String responseBody = getHtmlFile(filePathUrl); - - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.OK, responseHeader, responseBody); - } - private boolean isLogin(HttpCookie cookie) { - return cookie.isExist(JSESSIONID) - && SessionManager.InstanceOf().findSession(cookie.findCookie(JSESSIONID)) != null; - } - - private Optional handleDefault(final HttpRequest request) - throws URISyntaxException, IOException { - URL filePathUrl = getClass().getResource("/static" + request.getPath()); - if (filePathUrl == null) { - return Optional.empty(); + @Override + public void run() { + log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + process(connection); } - String responseBody = getHtmlFile(filePathUrl); - - HttpResponseHeader responseHeader = new HttpResponseHeader( - getContentType(request.getAccept(), request.getPath()), - String.valueOf(responseBody.getBytes().length), null, null); - return Optional.of(HttpResponse.of(HttpResponseStatus.OK, responseHeader, responseBody)); - - } - - private HttpResponse handle404() throws IOException, URISyntaxException { - URL filePathUrl = getClass().getResource("/static/404.html"); - - String responseBody = getHtmlFile(filePathUrl); - - HttpResponseHeader responseHeader = new HttpResponseHeader( - HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8, - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.NOT_FOUND, responseHeader, responseBody); - } - - private HttpResponse handle500() throws IOException, URISyntaxException { - String responseBody = getHtmlFile(getClass().getResource("/static/500.html")); - HttpResponseHeader responseHeader = new HttpResponseHeader( - HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8, - String.valueOf(responseBody.getBytes().length), null, null); - return HttpResponse.of(HttpResponseStatus.INTERNAL_SERVER_ERROR, responseHeader, responseBody); - } - - private String getHtmlFile(URL filePathUrl) throws URISyntaxException, IOException { - final Path filePath = Paths.get(Objects.requireNonNull(filePathUrl).toURI()); - return new String(Files.readAllBytes(filePath)); - } - private String getContentType(final String accept, final String uri) { - final String[] tokens = uri.split("\\."); - if ((tokens.length >= 1 && tokens[tokens.length - 1].equals("css")) || (accept != null && accept - .contains("text/css"))) { - return HttpResponseHeader.TEXT_CSS_CHARSET_UTF_8; + @Override + public void process(final Socket connection) { + try (final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream()) { + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + final HttpRequest httpRequest = HttpRequest.from(bufferedReader); + final Controller controller = RequestMapping.getController(httpRequest); + final HttpResponse response = new HttpResponse(); + controller.service(httpRequest, response); + outputStream.write( + response.toString() + .getBytes() + ); + outputStream.flush(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } } - return HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8; - } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java deleted file mode 100644 index 4df6c885f7..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.HashMap; -import java.util.Map; - -public class HttpCookie { - - private final Map requestCookies; - - public HttpCookie(String cookie) { - this.requestCookies = new HashMap<>(); - if (cookie == null) { - return; - } - String[] cookies = cookie.split("; "); - for (int i = 0; i < cookies.length; i++) { - String[] cookieTokens = cookies[i].split("="); - requestCookies.put(cookieTokens[0], cookieTokens[1]); - } - } - - public boolean isExist(String key) { - return requestCookies.get(key) != null; - } - - public String findCookie(String key) { - return requestCookies.get(key); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java deleted file mode 100644 index 8ad8173ff7..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.apache.coyote.http11; - -public enum HttpMethod { - GET, - POST; -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java deleted file mode 100644 index 6c969d07b3..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class HttpRequest { - - private HttpMethod method; - private String path; - private Map queryProperties; - - private Map headers; - - private String body; - - public HttpRequest(final String startLine, final Map headers, - final String body) { - final List startLineTokens = List.of(startLine.split(" ")); - this.method = HttpMethod.valueOf(startLineTokens.get(0)); - String uri = startLineTokens.get(1); - int uriSeparatorIndex = uri.indexOf("?"); - if (uriSeparatorIndex == -1) { - this.path = uri; - } else { - this.path = uri.substring(0, uriSeparatorIndex); - this.queryProperties = makeQueryProperties(uri.substring(uriSeparatorIndex + 1)); - } - this.headers = headers; - this.body = body; - - } - - private Map makeQueryProperties(final String queryString) { - Map result = new HashMap<>(); - String[] queryTokens = queryString.split("&"); - for (String queryToken : queryTokens) { - int equalSeparatorIndex = queryToken.indexOf("="); - if (equalSeparatorIndex != -1) { - result.put(queryToken.substring(0, equalSeparatorIndex), - queryToken.substring(equalSeparatorIndex + 1)); - } - } - return result; - } - - public HttpCookie getCookie() { - return new HttpCookie(this.headers.get("Cookie")); - } - - public boolean isSamePath(String path) { - return this.path.equals(path); - } - - public boolean isPOST() { - return this.method.equals(HttpMethod.POST); - } - - public boolean isGET() { - return this.method.equals(HttpMethod.GET); - } - - public boolean isNotExistBody() { - return this.body == null; - } - - public String getBody() { - return this.body; - } - - public String getAccept() { - return this.headers.get("Accept"); - } - - public String getPath() { - return this.path; - } - -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java deleted file mode 100644 index b00f3ccbd1..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.apache.coyote.http11; - -public class HttpResponse { - - private HttpResponseStatus responseStatus; - private HttpResponseHeader responseHeader; - private String responseBody; - - public static HttpResponse of(final HttpResponseStatus responseStatus, - final HttpResponseHeader responseHeader, final String responseBody) { - return new HttpResponse(responseStatus, responseHeader, responseBody); - } - - private HttpResponse(final HttpResponseStatus responseStatus, - final HttpResponseHeader responseHeader, final String responseBody) { - this.responseStatus = responseStatus; - this.responseHeader = responseHeader; - this.responseBody = responseBody; - } - - @Override - public String toString() { - return String.join("\r\n", - "HTTP/1.1 " + responseStatus.toString() + " ", - responseHeader.toString() + " ", - "", - responseBody); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseHeader.java deleted file mode 100644 index 05dd47c377..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseHeader.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class HttpResponseHeader { - - public static final String TEXT_HTML_CHARSET_UTF_8 = "text/html;charset=utf-8"; - public static final String TEXT_CSS_CHARSET_UTF_8 = "text/css;charset=utf-8"; - private Map headers; - - public HttpResponseHeader(final String contentType, final String contentLength, - final String location, final String setCookie) { - this.headers = new HashMap<>(); - headers.put("Content-Type", contentType); - headers.put("Content-Length", contentLength); - headers.put("Location", location); - headers.put("Set-Cookie", setCookie); - } - - @Override - public String toString() { - List header = headers.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .map(entry -> entry.getKey() + ": " + entry.getValue() - ).collect(Collectors.toList()); - return String.join(" \r\n", header); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatus.java deleted file mode 100644 index fe1d8f53a1..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseStatus.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.apache.coyote.http11; - -public enum HttpResponseStatus { - OK(200, "OK"), - FOUND(302, "Found"), - UNAUTHORIZATION(401, "Unauthorization"), - NOT_FOUND(404, "Not Found"), - INTERNAL_SERVER_ERROR(500, "Internal Server Error"); - private final int code; - private final String message; - - HttpResponseStatus(final int code, final String message) { - this.code = code; - this.message = message; - } - - @Override - public String toString() { - return code + " " + message; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java new file mode 100644 index 0000000000..b2c0590dfa --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapping.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11; + +import nextstep.jwp.controller.*; +import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.controller.DefaultController; +import org.apache.coyote.http11.request.HttpRequest; + +import java.util.List; + +public class RequestMapping { + private static final List CONTROLLER_INSTANCES = List.of( + new LoginGetController(), + new LoginPostController(), + new RegisterGetController(), + new RegisterPostController(), + new DefaultGetController() + ); + + private RequestMapping() { + } + + public static Controller getController(HttpRequest request) { + return CONTROLLER_INSTANCES.stream() + .filter(controller -> controller.isSupported(request)) + .findFirst() + .orElse(new DefaultController()); + } +} + diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java new file mode 100644 index 0000000000..dc15b4cdcb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/AbstractController.java @@ -0,0 +1,86 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.exception.UnauthorizeException; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +public abstract class AbstractController implements Controller { + + public static final int EXTENSION_TOKENS_MIN_SIZE = 1; + + @Override + public void service(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + try { + handleRequest(request, response); + } catch (UnauthorizeException e) { + handle401(request, response); + } catch (Exception e) { + handle500(response); + } + } + + private void handleRequest(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + if (request.isGET()) { + doGet(request, response); + } + if (request.isPOST()) { + doPost(request, response); + } + } + + protected void doPost(HttpRequest request, HttpResponse response) { + } + + protected void doGet(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + } + + protected String readHtmlFile(URL filePathUrl) throws URISyntaxException, IOException { + final Path filePath = Paths.get(Objects.requireNonNull(filePathUrl).toURI()); + return new String(Files.readAllBytes(filePath)); + } + + protected String readContentType(final String accept, final String uri) { + final String[] tokens = uri.split("\\."); + if (isExtensionCss(tokens) || isAcceptCss(accept)) { + return HttpResponseHeader.TEXT_CSS_CHARSET_UTF_8; + } + return HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8; + } + + private boolean isExtensionCss(String[] tokens) { + return tokens.length >= EXTENSION_TOKENS_MIN_SIZE && tokens[tokens.length - 1].equals("css"); + } + + private boolean isAcceptCss(String accept) { + return accept != null && accept.contains("text/css"); + } + + private void handle401(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException { + String responseBody = readHtmlFile(getClass().getResource("/static/401.html")); + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.UNAUTHORIZATION, responseHeader, responseBody); + } + + private void handle500(HttpResponse response) throws IOException, URISyntaxException { + String responseBody = readHtmlFile(getClass().getResource("/static/500.html")); + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8, + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, responseHeader, responseBody); + } +} + diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java new file mode 100644 index 0000000000..2077da9f70 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java @@ -0,0 +1,14 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.net.URISyntaxException; + +public interface Controller { + boolean isSupported(HttpRequest request); + + void service(HttpRequest request, HttpResponse response) throws URISyntaxException, IOException; +} + diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/DefaultController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/DefaultController.java new file mode 100644 index 0000000000..c9b9f6c111 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/DefaultController.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpResponseHeader; +import org.apache.coyote.http11.response.HttpResponseStatus; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; + +public class DefaultController extends AbstractController { + @Override + public boolean isSupported(HttpRequest request) { + return true; + } + + @Override + public void service(HttpRequest request, HttpResponse response) throws IOException, URISyntaxException { + URL filePathUrl = getClass().getResource("/static" + request.getPath()); + if (filePathUrl == null) { + handle404(response); + return; + } + String responseBody = readHtmlFile(filePathUrl); + + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + readContentType(request.getAccept(), request.getPath()), + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.OK, responseHeader, responseBody); + } + + private void handle404(HttpResponse response) throws IOException, URISyntaxException { + URL filePathUrl = getClass().getResource("/static/404.html"); + + String responseBody = readHtmlFile(filePathUrl); + + HttpResponseHeader responseHeader = new HttpResponseHeader.Builder( + HttpResponseHeader.TEXT_HTML_CHARSET_UTF_8, + String.valueOf(responseBody.getBytes().length)) + .build(); + response.updateResponse(HttpResponseStatus.NOT_FOUND, responseHeader, responseBody); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizeException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizeException.java new file mode 100644 index 0000000000..acc039d25c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/UnauthorizeException.java @@ -0,0 +1,7 @@ +package org.apache.coyote.http11.exception; + +public class UnauthorizeException extends RuntimeException { + public UnauthorizeException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java new file mode 100644 index 0000000000..f17ce29584 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class HttpCookie { + + private final Map requestCookies; + + public static HttpCookie from(String cookie) { + Map requestCookies = new HashMap<>(); + if (cookie != null) { + String[] cookies = cookie.split("; "); + for (int i = 0; i < cookies.length; i++) { + String[] cookieTokens = cookies[i].split("="); + requestCookies.put(cookieTokens[0], cookieTokens[1]); + } + } + return new HttpCookie(requestCookies); + } + + private HttpCookie(Map requestCookies) { + this.requestCookies = requestCookies; + } + + public boolean isExist(String key) { + return requestCookies.get(key) != null; + } + + public String findCookie(String key) { + return requestCookies.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..024086f1f4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,6 @@ +package org.apache.coyote.http11.request; + +public enum HttpMethod { + GET, + POST +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..f3d6794ddd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,64 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Map; + +public class HttpRequest { + + private final HttpRequestStartLine startLine; + + private final HttpRequestHeader header; + + private final HttpRequestBody body; + + public static HttpRequest from(final BufferedReader bufferedReader) throws IOException { + final HttpRequestStartLine startLine = HttpRequestStartLine.from(bufferedReader.readLine()); + final HttpRequestHeader httpRequestHeaders = HttpRequestHeader.from(bufferedReader); + final HttpRequestBody httpRequestBody = HttpRequestBody.from(httpRequestHeaders.getContentLength(), bufferedReader); + return new HttpRequest(startLine, httpRequestHeaders, httpRequestBody); + } + + private HttpRequest(final HttpRequestStartLine startLine, final HttpRequestHeader header, final HttpRequestBody body) { + this.startLine = startLine; + this.header = header; + this.body = body; + } + + public Map parseBody() { + return this.body.parse(); + } + + public boolean isPOST() { + return this.startLine.isPOST(); + } + + public boolean isGET() { + return this.startLine.isGET(); + } + + public boolean isNotExistBody() { + return this.body == null; + } + + public boolean isSamePath(String path) { + return this.startLine.isSamePath(path); + } + + public String getAccept() { + return this.header.getAccept(); + } + + public HttpCookie getCookie() { + return this.header.getCookie(); + } + + public String getPath() { + return this.startLine.getPath(); + } + + public HttpRequestBody getBody() { + return this.body; + } + +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java new file mode 100644 index 0000000000..5580aceac2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java @@ -0,0 +1,42 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class HttpRequestBody { + private final String body; + + public static HttpRequestBody from(final String contentLength, final BufferedReader bufferedReader) throws IOException { + if (contentLength == null) { + return new HttpRequestBody(""); + } + int length = Integer.parseInt(contentLength.trim()); + char[] buffer = new char[length]; + bufferedReader.read(buffer, 0, length); + return new HttpRequestBody(new String(buffer)); + } + + + public Map parse() { + Map parsedRequestBody = new HashMap<>(); + String[] queryTokens = body.split("&"); + for (String queryToken : queryTokens) { + putRequestBodyToken(queryToken, parsedRequestBody); + } + return parsedRequestBody; + } + + private void putRequestBodyToken(String queryToken, Map parsedRequestBody) { + int equalSeparatorIndex = queryToken.indexOf("="); + if (equalSeparatorIndex != -1) { + parsedRequestBody.put(queryToken.substring(0, equalSeparatorIndex), + queryToken.substring(equalSeparatorIndex + 1)); + } + } + + private HttpRequestBody(final String body) { + this.body = body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeader.java new file mode 100644 index 0000000000..163ad2c93e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeader.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class HttpRequestHeader { + private final Map headers; + + public static HttpRequestHeader from(final BufferedReader bufferedReader) throws IOException { + Map parsedHeaders = new HashMap<>(); + String line = bufferedReader.readLine(); + while (!"".equals(line)) { + String[] headerTokens = line.split(": "); + parsedHeaders.put(headerTokens[0], headerTokens[1]); + line = bufferedReader.readLine(); + } + return new HttpRequestHeader(parsedHeaders); + } + + private HttpRequestHeader(final Map headers) { + this.headers = headers; + } + + public HttpCookie getCookie() { + return HttpCookie.from(this.headers.get("Cookie")); + } + + public String getAccept() { + return this.headers.get("Accept"); + } + + public String getContentLength() { + return this.headers.get("Content-Length"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java new file mode 100644 index 0000000000..1f18ddc138 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.request; + +import java.util.List; + +public class HttpRequestStartLine { + private final HttpMethod method; + private final String path; + + private HttpRequestStartLine(final HttpMethod method, final String path) { + this.method = method; + this.path = path; + } + + public static HttpRequestStartLine from(final String startLine) { + if (startLine == null) { + throw new IllegalArgumentException("HTTP 요청이 올바르게 입력되지 않았습니다."); + } + final List startLineTokens = List.of(startLine.split(" ")); + final HttpMethod method = HttpMethod.valueOf(startLineTokens.get(0)); + return makeStartLine(startLineTokens.get(1), method); + } + + private static HttpRequestStartLine makeStartLine(String uri, HttpMethod method) { + int uriSeparatorIndex = uri.indexOf("?"); + if (uriSeparatorIndex == -1) { + return new HttpRequestStartLine(method, uri); + } + return new HttpRequestStartLine(method, uri.substring(0, uriSeparatorIndex)); + } + + public boolean isPOST() { + return this.method.equals(HttpMethod.POST); + } + + public boolean isGET() { + return this.method.equals(HttpMethod.GET); + } + + public boolean isSamePath(String path) { + return this.path.equals(path); + } + + public String getPath() { + return this.path; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..51845e94d3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.response; + +public class HttpResponse { + public static final String DEFAULT_PROTOCOL_VERSION = "HTTP/1.1"; + private String protocolVersion; + private HttpResponseStatus responseStatus; + private HttpResponseHeader responseHeader; + private String responseBody; + + public void updateResponse(final HttpResponseStatus responseStatus, final HttpResponseHeader responseHeader, final String responseBody) { + this.protocolVersion = DEFAULT_PROTOCOL_VERSION; + this.responseStatus = responseStatus; + this.responseHeader = responseHeader; + this.responseBody = responseBody; + } + + @Override + public String toString() { + return String.join("\r\n", + protocolVersion + " " + responseStatus.toString() + " ", + responseHeader.toString() + " ", + "", + responseBody); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeader.java new file mode 100644 index 0000000000..b16fdb02df --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeader.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpResponseHeader { + + public static final String TEXT_HTML_CHARSET_UTF_8 = "text/html;charset=utf-8"; + public static final String TEXT_CSS_CHARSET_UTF_8 = "text/css;charset=utf-8"; + private final Map headers; + + public HttpResponseHeader(final Builder httpResponseHeaderBuilder) { + this.headers = httpResponseHeaderBuilder.headers; + } + + @Override + public String toString() { + List header = headers.entrySet().stream() + .filter(entry -> entry.getValue() != null) + .map(entry -> entry.getKey() + ": " + entry.getValue() + ).collect(Collectors.toList()); + return String.join(" \r\n", header); + } + + public static class Builder { + private Map headers; + + public Builder(final String contentType, final String contentLength) { + this.headers = new HashMap<>(); + this.headers.put("Content-Type", contentType); + this.headers.put("Content-Length", contentLength); + } + + public Builder addLocation(final String location) { + this.headers.put("Location", location); + return this; + } + + public Builder addSetCookie(final String setCookie) { + this.headers.put("Set-Cookie", setCookie); + return this; + } + + public HttpResponseHeader build() { + return new HttpResponseHeader(this); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatus.java new file mode 100644 index 0000000000..2ab260efaf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatus.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.response; + +public enum HttpResponseStatus { + OK(200, "OK"), + FOUND(302, "Found"), + UNAUTHORIZATION(401, "Unauthorization"), + NOT_FOUND(404, "Not Found"), + INTERNAL_SERVER_ERROR(500, "Internal Server Error"); + private final int code; + private final String message; + + HttpResponseStatus(final int code, final String message) { + this.code = code; + this.message = message; + } + + @Override + public String toString() { + return code + " " + message; + } +} 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 e23fc57f3b..d27435d9b5 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 @@ -1,61 +1,317 @@ package nextstep.org.apache.coyote.http11; -import static org.assertj.core.api.Assertions.assertThat; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import support.StubSocket; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; -import org.apache.coyote.http11.Http11Processor; -import org.junit.jupiter.api.Test; -import support.StubSocket; + +import static org.assertj.core.api.Assertions.assertThat; class Http11ProcessorTest { + @Nested + @DisplayName("GET 요청에 대한 테스트") + class GetTest { + @Test + @DisplayName("/ url로 접속하면 hello world가 로드된다.") + void process() { + // given + final var socket = new StubSocket(); + final var processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + var expected = String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Length: 12 ", + "Content-Type: text/html;charset=utf-8 ", + "", + "Hello world!"); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("/index.html url로 접속하면 대시보드 페이지가 로드된다.") + void index() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /index.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Length: 5564 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("회원가입 화면에 접속하면 회원가입 페이지가 로드된다.") + void registerGet() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + final URL resource = getClass().getClassLoader().getResource("static/register.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Length: 4319 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("현재 저장된 세션이 없을 때, 로그인 화면에 접속하면 로그인 페이지가 로드된다.") + void loginGet() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Length: 2849 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("현재 저장된 세션이 존재할 때, 로그인 화면에 접속하면 /index.html로 리다이렉트된다.") + void loginGet_already_login() { + // given + String sessionId = createSessionId(); + final String httpRequest = String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Cookie: JSESSIONID=" + sessionId, + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + var expected = "HTTP/1.1 302 Found \r\n" + + "Content-Length: 0 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Location: /index.html \r\n" + + "\r\n"; + + assertThat(socket.output()).isEqualTo(expected); + } + + private String createSessionId() { + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 30 ", + "Content-Type: application/x-www-form-urlencoded ", + "", + "account=gugu&password=password"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + processor.process(socket); + + return socket.output().split("JSESSIONID=")[1].split(" \r\n")[0].trim(); + } + + @Test + @DisplayName("유효하지 않은 url로 접속할 경우 404.html 파일이 로드된다.") + void load_404() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /test HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + final URL resource = getClass().getClassLoader().getResource("static/404.html"); + var expected = "HTTP/1.1 404 Not Found \r\n" + + "Content-Length: 2426 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + } + + @Nested + @DisplayName("POST 요청에 대한 테스트") + class PostTest { + @Test + @DisplayName("/register에 POST 요청을 보내면 index.html로 리다이렉트하는 302 응답이 반환된다.") + void register() { + // given + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 57 ", + "Content-Type: application/x-www-form-urlencoded ", + "", + "account=amaranth&email=amaranth%40naver.com&password=test"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + var registerActual = socket.output(); + + loginAfterRegister(socket); + var loginActual = socket.output(); + + // then + var expected = "HTTP/1.1 302 Found \r\n" + + "Content-Length: 0 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Location: /index.html \r\n" + + "\r\n"; + + assertThat(registerActual).isEqualTo(expected); + + var expectedStart = "HTTP/1.1 302 Found \r\n"; + assertThat(loginActual).startsWith(expectedStart); + } + + private void loginAfterRegister(StubSocket socket) { + final String loginRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 30 ", + "Content-Type: application/x-www-form-urlencoded ", + "", + "account=amaranth&password=test"); + + final Http11Processor processor = new Http11Processor(new StubSocket(loginRequest)); + + processor.process(socket); + } + + @Test + @DisplayName("/login에 존재하지 않는 회원의 정보를 담은 POST 요청을 보내면 401.html 파일이 로드된다.") + void login_fail() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 57 ", + "Content-Type: application/x-www-form-urlencoded ", + "", + "account=amaranth&password=test"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + final URL resource = getClass().getClassLoader().getResource("static/401.html"); + var expected = "HTTP/1.1 401 Unauthorization \r\n" + + "Content-Length: 2426 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + @DisplayName("/login에 POST 요청을 보내면 Set-Cookie에 세션 아이디가 포함된 302 응답이 반환된다.") + void login() { + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 30 ", + "Content-Type: application/x-www-form-urlencoded ", + "", + "account=gugu&password=password"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + // then + var expectedStart = "HTTP/1.1 302 Found \r\n"; + var expectedHeaderProperty = "Set-Cookie: JSESSIONID="; + var expectedEnd = "Content-Length: 0 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Location: /index.html \r\n" + + "\r\n"; + var actual = socket.output(); + Assertions.assertAll( + () -> assertThat(actual).startsWith(expectedStart), + () -> assertThat(actual).contains(expectedHeaderProperty), + () -> assertThat(actual).endsWith(expectedEnd) + ); + } + + } + - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Length: 12 ", - "Content-Type: text/html;charset=utf-8 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - - @Test - void index() throws IOException { - // given - final String httpRequest = String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Length: 5564 \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "\r\n" + - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - - assertThat(socket.output()).isEqualTo(expected); - } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java b/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java new file mode 100644 index 0000000000..b6a81a30ba --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/RequestMappingTest.java @@ -0,0 +1,140 @@ +package org.apache.coyote.http11; + +import nextstep.jwp.controller.*; +import org.apache.coyote.http11.controller.Controller; +import org.apache.coyote.http11.controller.DefaultController; +import org.apache.coyote.http11.request.HttpRequest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +class RequestMappingTest { + + @Test + @DisplayName("getController() 메서드에 / url에 보내는 GET 요청을 입력하면 DefaultGetController 객체를 반환한다.") + void getController_default_get() throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + "GET / HTTP/1.1 ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(DefaultGetController.class); + } + + @Test + @DisplayName("getController() 메서드에 /login url에 보내는 GET 요청을 입력하면 LoginGetController 객체를 반환한다.") + void getController_login_get() throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + "GET /login HTTP/1.1 ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(LoginGetController.class); + } + + @Test + @DisplayName("getController() 메서드에 /login url에 보내는 POST 요청을 입력하면 LoginPostController 객체를 반환한다.") + void getController_login_post() throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(LoginPostController.class); + } + + @Test + @DisplayName("getController() 메서드에 /register url에 보내는 GET 요청을 입력하면 RegisterGetController 객체를 반환한다.") + void getController_register_get() throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + "GET /register HTTP/1.1 ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(RegisterGetController.class); + } + + @Test + @DisplayName("getController() 메서드에 /register url에 보내는 POST 요청을 입력하면 RegisterPostController 객체를 반환한다.") + void getController_register_post() throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(RegisterPostController.class); + } + + @ParameterizedTest + @ValueSource(strings = {"GET /test HTTP/1.1", "POST / HTTP/1.1"}) + @DisplayName("getController() 메서드에 매핑되는 컨트롤러가 존재하지 않는 요청을 입력하면 DefaultController 객체를 반환한다.") + void getController(final String startLine) throws IOException { + //given + final BufferedReader reader = new BufferedReader( + new StringReader(String.join("\r\n", + startLine + " ", + "Host: localhost:8080 ", + "", + "") + )); + final HttpRequest request = HttpRequest.from(reader); + + //when + Controller controller = RequestMapping.getController(request); + + //then + Assertions.assertThat(controller).isInstanceOf(DefaultController.class); + } + +} \ No newline at end of file diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpCookieTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpCookieTest.java new file mode 100644 index 0000000000..6de900cdca --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpCookieTest.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.request; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class HttpCookieTest { + + @ParameterizedTest + @CsvSource(value = {"test=aaa:false", "JSESSION=abcdefg:true"}, delimiter = ':') + @DisplayName("isExist()를 호출하면 쿠키의 존재 여부를 반환한다.") + void isExist(final String rawCookie, final boolean expected) { + //given + final HttpCookie cookie = HttpCookie.from(rawCookie); + + //when + final boolean actual = cookie.isExist("JSESSION"); + + //then + Assertions.assertThat(actual).isEqualTo(expected); + + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestBodyTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestBodyTest.java new file mode 100644 index 0000000000..85daef2db6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestBodyTest.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.request; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +class HttpRequestBodyTest { + + @Test + @DisplayName("") + void parse() throws IOException { + //given + String body = "account=gugu&password=password"; + final HttpRequestBody requestBody = HttpRequestBody.from("30", new BufferedReader(new StringReader(body))); + final Map actual = Map.of("account", "gugu", "password", "password"); + + //when + final Map expected = requestBody.parse(); + + //then + Assertions.assertThat(expected) + .usingDefaultComparator() + .usingRecursiveComparison() + .isEqualTo(actual); + } + +} \ No newline at end of file diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java new file mode 100644 index 0000000000..0cfcff6dec --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java @@ -0,0 +1,53 @@ +package org.apache.coyote.http11.request; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class HttpRequestStartLineTest { + + @ParameterizedTest + @CsvSource(value = {"GET / HTTP/1.1:false", "POST / HTTP1.1:true"}, delimiter = ':') + @DisplayName("요청에 대해 isPost()를 호출하면 POST 요청인지 여부를 반환한다.") + void isPost(final String startLine, final boolean expected) { + //given + final HttpRequestStartLine httpRequestStartLine = HttpRequestStartLine.from(startLine); + + //when + final boolean actual = httpRequestStartLine.isPOST(); + + //then + Assertions.assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"GET / HTTP/1.1:true", "POST / HTTP1.1:false"}, delimiter = ':') + @DisplayName("요청에 대해 isGet()를 호출하면 POST 요청인지 여부를 반환한다.") + void isGET(final String startLine, final boolean expected) { + //given + final HttpRequestStartLine httpRequestStartLine = HttpRequestStartLine.from(startLine); + + //when + final boolean actual = httpRequestStartLine.isGET(); + + //then + Assertions.assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ValueSource(strings = {"GET /login HTTP/1.1", "GET /login?a=b HTTP1.1"}) + @DisplayName("요청에 대해 isSamePath()에 '/login'을 입력하면 요청의 true를 반환한다.") + void isSamePath(final String startLine) { + //given + final HttpRequestStartLine httpRequestStartLine = HttpRequestStartLine.from(startLine); + + //when + final boolean actual = httpRequestStartLine.isSamePath("/login"); + + //then + Assertions.assertThat(actual).isTrue(); + } + +}