diff --git a/README.md b/README.md index 70be385f28..83658875c0 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,11 @@ - [x] 회원가입 페이지의 경우 GET을 사용하여 보여준다. - [x] 회원가입의 경우 POST를 사용한다. - [x] 회원가입을 완료하는 경우 index.html로 리다이렉트한다. - +- [x] 리팩터링을 진행한다. + - [x] HttpRequest 클래스를 구현한다. + - [x] HttpResponse 클래스를 구현한다. + - [x] 알 수 없는 예외 발생시 Internal Server Error 페이지 반환한다. + - [x] Controller 인터페이스를 추가한다. + - [x] Controller를 매핑해주는 클래스를 구현한다. +- [x] ThreadPoolExecutor를 사용해서 스레드 풀(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..177d2786c1 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,23 @@ 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 CacheControl cacheControl = CacheControl + .noCache() + .cachePrivate(); + + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, "/**"); + + registry.addInterceptor(webContentInterceptor); } } 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..d83bc57da0 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,18 @@ package cache.com.example.etag; +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 var filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addUrlPatterns("/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..57504d8dbd 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.util.concurrent.TimeUnit; 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; @@ -20,6 +22,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(365, TimeUnit.DAYS).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..17ef63cf79 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -3,7 +3,13 @@ handlebars: server: tomcat: - accept-count: 1 - max-connections: 1 +# 모든 쓰레드가 사용 중 일 때 들어온 요청이 대기하는 최대 큐의 길이 + accept-count: 2 + # 서버가 유지할 수 있는 최대 Connection의 수 + max-connections: 2 + # 최대 실행 가능 Thread 수 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..6297f3dfdf 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -1,29 +1,24 @@ 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 { @@ -41,7 +36,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..afe5ff6db2 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()); diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index 3dd7593507..28bbffe0c6 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,11 +1,19 @@ package nextstep; +import java.util.Map; +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; import org.apache.catalina.startup.Tomcat; public class Application { public static void main(String[] args) { - final var tomcat = new Tomcat(); + final Tomcat tomcat = new Tomcat(Map.of( + "/", new HomeController(), + "/login", new LoginController(), + "/register", new RegisterController() + )); tomcat.start(); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java new file mode 100644 index 0000000000..def4797c22 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,17 @@ +package nextstep.jwp.controller; + +import org.apache.catalina.controller.AbstractController; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public class HomeController extends AbstractController { + + private static final String HOME_PAGE = "/home.html"; + + @Override + protected void doGet(final HttpRequest request, final HttpResponse response) { + response.setHttpStatus(HttpStatus.OK) + .sendRedirect(HOME_PAGE); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java new file mode 100644 index 0000000000..e0d1f50109 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,50 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.controller.AbstractController; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.Session; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.response.HttpResponse; + +public class LoginController extends AbstractController { + + private static final String INDEX_PAGE = "/index.html"; + private static final String LOGIN_PAGE = "/login.html"; + private static final String UNAUTHORIZED_PAGE = "/401.html"; + + @Override + protected void doPost(final HttpRequest request, final HttpResponse response) { + final RequestBody requestBody = request.getRequestBody(); + final String account = requestBody.get("account"); + final String password = requestBody.get("password"); + + final User user = InMemoryUserRepository.findByAccount(account).orElseThrow(); + if (!user.checkPassword(password)) { + response.setHttpStatus(HttpStatus.UNAUTHORIZED).sendRedirect(UNAUTHORIZED_PAGE); + return; + } + + final Session session = request.getSession(); + session.setAttribute("user", user); + response.setHttpStatus(HttpStatus.FOUND) + .setCookie("JSESSIONID", session.getId()) + .setSession(session) + .addHeader("Location", INDEX_PAGE) + .sendRedirect(INDEX_PAGE); + } + + @Override + protected void doGet(final HttpRequest request, final HttpResponse response) { + final Session session = request.getSession(); + if (session.getAttribute("user") == null) { + response.setHttpStatus(HttpStatus.OK).sendRedirect(LOGIN_PAGE); + return; + } + response.setHttpStatus(HttpStatus.FOUND) + .addHeader("Location", INDEX_PAGE) + .sendRedirect(INDEX_PAGE); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java new file mode 100644 index 0000000000..675550dfc8 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,37 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.controller.AbstractController; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.response.HttpResponse; + +public class RegisterController extends AbstractController { + + private static final String INDEX_PAGE = "/index.html"; + private static final String REGISTER_PAGE = "/register.html"; + private static final String CONFLICT_PAGE = "/409.html"; + + @Override + protected void doPost(final HttpRequest request, final HttpResponse response) { + final RequestBody requestBody = request.getRequestBody(); + final String account = requestBody.get("account"); + if (InMemoryUserRepository.findByAccount(account).isPresent()) { + response.setHttpStatus(HttpStatus.CONFLICT) + .sendRedirect(CONFLICT_PAGE); + return; + } + InMemoryUserRepository.save(new User(account, requestBody.get("password"), requestBody.get("email"))); + response.setHttpStatus(HttpStatus.FOUND) + .addHeader("Location", INDEX_PAGE) + .sendRedirect(INDEX_PAGE); + } + + @Override + protected void doGet(final HttpRequest request, final HttpResponse response) { + response.setHttpStatus(HttpStatus.OK) + .sendRedirect(REGISTER_PAGE); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..65e59b42e5 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -1,27 +1,35 @@ package nextstep.jwp.db; -import nextstep.jwp.model.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import nextstep.jwp.model.User; public class InMemoryUserRepository { private static final Map database = new ConcurrentHashMap<>(); + private static final long INITIAL_KEY_VALUE = 1L; + private static final AtomicLong id = new AtomicLong(INITIAL_KEY_VALUE); static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); + final User user = new User(id.getAndIncrement(), "gugu", "password", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } public static void save(User user) { - database.put(user.getAccount(), user); + final User savedUser = new User(id.getAndIncrement(), user.getAccount(), user.getPassword(), user.getEmail()); + database.put(savedUser.getAccount(), savedUser); } public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - private InMemoryUserRepository() {} + public static void clear() { + database.clear(); + } + + private InMemoryUserRepository() { + } } diff --git a/tomcat/src/main/java/nextstep/jwp/model/User.java b/tomcat/src/main/java/nextstep/jwp/model/User.java index 4c2a2cd184..06be6a6454 100644 --- a/tomcat/src/main/java/nextstep/jwp/model/User.java +++ b/tomcat/src/main/java/nextstep/jwp/model/User.java @@ -7,25 +7,44 @@ public class User { private final String password; private final String email; - public User(Long id, String account, String password, String email) { + public User(final String account, final String password, final String email) { + this(null, account, password, email); + } + + public User(final Long id, final String account, final String password, final String email) { + validate(account, password, email); this.id = id; this.account = account; this.password = password; this.email = email; } - public User(String account, String password, String email) { - this(null, account, password, email); + private void validate(final String account, final String password, final String email) { + if (account.isEmpty() || password.isEmpty() || email.isEmpty()) { + throw new IllegalArgumentException("올바른 사용자 정보를 입력해주세요."); + } } - public boolean checkPassword(String password) { + public boolean checkPassword(final String password) { return this.password.equals(password); } + public Long getId() { + return id; + } + public String getAccount() { return account; } + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + @Override public String toString() { return "User{" + diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..884657d510 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,56 +1,12 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; +import org.apache.coyote.http11.common.Session; -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. - *

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

- * - * @author Craig R. McClanahan - */ public interface Manager { - /** - * Add this Session to the set of active Sessions for this Manager. - * - * @param session Session to be added - */ - void add(HttpSession session); + void add(final 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 - * - * @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 - */ - HttpSession findSession(String id) throws IOException; + Session findSession(final String id); - /** - * Remove this Session from the active Sessions for this Manager. - * - * @param session Session to be removed - */ - void remove(HttpSession session); + void remove(final String id); } diff --git a/tomcat/src/main/java/org/apache/catalina/RequestAdapter.java b/tomcat/src/main/java/org/apache/catalina/RequestAdapter.java new file mode 100644 index 0000000000..6aed8fdc30 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/RequestAdapter.java @@ -0,0 +1,38 @@ +package org.apache.catalina; + +import org.apache.catalina.controller.Controller; +import org.apache.coyote.http11.Adapter; +import org.apache.coyote.http11.common.Session; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; + +public class RequestAdapter implements Adapter { + + private final RequestMapper requestMapper; + private final SessionManager sessionManager = new SessionManager(); + + public RequestAdapter(final RequestMapper requestMapper) { + this.requestMapper = requestMapper; + } + + @Override + public void service(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final RequestLine requestLine = httpRequest.getRequestLine(); + final Controller controller = requestMapper.getController(requestLine.parseUri()); + setSessionToHttpRequest(httpRequest); + controller.service(httpRequest, httpResponse); + addSession(httpResponse); + } + + private void setSessionToHttpRequest(final HttpRequest httpRequest) { + final String sessionId = httpRequest.parseSessionId(); + final Session session = sessionManager.findSession(sessionId); + httpRequest.setSession(session); + } + + private void addSession(final HttpResponse httpResponse) { + final Session session = httpResponse.getSession(); + sessionManager.add(session); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/RequestMapper.java b/tomcat/src/main/java/org/apache/catalina/RequestMapper.java new file mode 100644 index 0000000000..6ba3b15230 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/RequestMapper.java @@ -0,0 +1,20 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; +import org.apache.catalina.controller.Controller; +import org.apache.catalina.controller.StaticController; + +public class RequestMapper { + + private final Controller defaultController = new StaticController(); + private final Map controllers = new HashMap<>(); + + public RequestMapper(final Map controllers) { + this.controllers.putAll(controllers); + } + + public Controller getController(final String uri) { + return controllers.getOrDefault(uri, defaultController); + } +} 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..cb27ea0030 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -0,0 +1,32 @@ +package org.apache.catalina; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.coyote.http11.common.Session; + +public class SessionManager implements Manager { + + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public void add(final Session session) { + if (session == null) { + return; + } + sessions.put(session.getId(), session); + } + + @Override + public Session findSession(final String id) { + return sessions.getOrDefault(id, new Session()); + } + + @Override + public void remove(final String id) { + sessions.remove(id); + } + + public void clear() { + sessions.clear(); + } +} 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 d171bb84a8..38b11084ad 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.http11.Adapter; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,20 +14,23 @@ public class Connector implements Runnable { private static final Logger log = LoggerFactory.getLogger(Connector.class); - private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; private final ServerSocket serverSocket; private boolean stopped; + private final Adapter adapter; + private final ExecutorService executorService; - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + public Connector(final Adapter adapter, final int threadCount) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, adapter, threadCount); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, final Adapter adapter, final int threadCount) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.adapter = adapter; + this.executorService = Executors.newFixedThreadPool(threadCount); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -38,7 +44,7 @@ private ServerSocket createServerSocket(final int port, final int acceptCount) { } public void start() { - var thread = new Thread(this); + final Thread thread = new Thread(this); thread.setDaemon(true); thread.start(); stopped = false; @@ -65,8 +71,8 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); - new Thread(processor).start(); + final Http11Processor processor = new Http11Processor(connection, adapter); + executorService.execute(processor); } public void stop() { @@ -79,8 +85,8 @@ public void stop() { } private int checkPort(final int port) { - final var MIN_PORT = 1; - final var MAX_PORT = 65535; + final int MIN_PORT = 1; + final int MAX_PORT = 65535; if (port < MIN_PORT || MAX_PORT < port) { return DEFAULT_PORT; diff --git a/tomcat/src/main/java/org/apache/catalina/controller/AbstractController.java b/tomcat/src/main/java/org/apache/catalina/controller/AbstractController.java new file mode 100644 index 0000000000..bcad07bf39 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/controller/AbstractController.java @@ -0,0 +1,38 @@ +package org.apache.catalina.controller; + +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public abstract class AbstractController implements Controller { + + private static final String BAD_REQUEST_PAGE = "/400.html"; + private static final String NOT_FOUND_PAGE = "/404.html"; + + @Override + public void service(final HttpRequest request, final HttpResponse response) { + try { + routing(request, response); + } catch (final IllegalArgumentException e) { + response.setHttpStatus(HttpStatus.BAD_REQUEST).sendRedirect(BAD_REQUEST_PAGE); + } + } + + private void routing(final HttpRequest request, final HttpResponse response) { + if (request.isGet()) { + doGet(request, response); + return; + } + if (request.isPost()) { + doPost(request, response); + return; + } + response.setHttpStatus(HttpStatus.NOT_FOUND).sendRedirect(NOT_FOUND_PAGE); + } + + protected void doPost(HttpRequest request, HttpResponse response) { + } + + protected void doGet(HttpRequest request, HttpResponse response) { + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/controller/Controller.java b/tomcat/src/main/java/org/apache/catalina/controller/Controller.java new file mode 100644 index 0000000000..4285447029 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/controller/Controller.java @@ -0,0 +1,8 @@ +package org.apache.catalina.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public interface Controller { + void service(final HttpRequest request, final HttpResponse response); +} diff --git a/tomcat/src/main/java/org/apache/catalina/controller/StaticController.java b/tomcat/src/main/java/org/apache/catalina/controller/StaticController.java new file mode 100644 index 0000000000..56485faaa7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/controller/StaticController.java @@ -0,0 +1,30 @@ +package org.apache.catalina.controller; + +import static org.apache.coyote.http11.common.HttpStatus.NOT_FOUND; +import static org.apache.coyote.http11.common.HttpStatus.OK; + +import java.net.URL; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; + +public class StaticController extends AbstractController { + + private static final String STATIC_RESOURCE_PREFIX = "static"; + private static final String NOT_FOUND_PAGE = "/404.html"; + + private final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + + @Override + protected void doGet(final HttpRequest request, final HttpResponse response) { + final RequestLine requestLine = request.getRequestLine(); + final String uri = requestLine.parseUri(); + + final URL resource = classLoader.getResource(STATIC_RESOURCE_PREFIX + uri); + if (resource == null) { + response.setHttpStatus(NOT_FOUND).sendRedirect(NOT_FOUND_PAGE); + return; + } + response.setHttpStatus(OK).sendRedirect(uri); + } +} 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..7d52dd56e4 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,17 +1,30 @@ package org.apache.catalina.startup; +import java.io.IOException; +import java.util.Map; +import org.apache.catalina.RequestAdapter; +import org.apache.catalina.RequestMapper; import org.apache.catalina.connector.Connector; +import org.apache.catalina.controller.Controller; +import org.apache.coyote.http11.Adapter; 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 static final int DEFAULT_THREAD_COUNT = 250; + + private final Adapter adapter; + + public Tomcat(final Map controllers) { + final RequestMapper requestMapper = new RequestMapper(controllers); + this.adapter = new RequestAdapter(requestMapper); + } public void start() { - var connector = new Connector(); + final Connector connector = new Connector(adapter, DEFAULT_THREAD_COUNT); + connector.start(); try { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Adapter.java b/tomcat/src/main/java/org/apache/coyote/http11/Adapter.java new file mode 100644 index 0000000000..3fe8c770ff --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Adapter.java @@ -0,0 +1,9 @@ +package org.apache.coyote.http11; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public interface Adapter { + + void service(final HttpRequest httpRequest, final HttpResponse httpResponse); +} 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 71a73715f1..ef5be04ed3 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,52 +1,42 @@ package org.apache.coyote.http11; -import static org.apache.coyote.http11.common.Constants.CRLF; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; -import java.util.UUID; -import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; -import nextstep.jwp.model.User; import org.apache.coyote.Processor; -import org.apache.coyote.http11.common.HttpCookie; -import org.apache.coyote.http11.common.HttpMethod; import org.apache.coyote.http11.common.HttpStatus; -import org.apache.coyote.http11.common.Session; -import org.apache.coyote.http11.common.SessionManager; -import org.apache.coyote.http11.request.RequestBody; -import org.apache.coyote.http11.request.RequestHeader; -import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestParser; +import org.apache.coyote.http11.response.HttpResponse; import org.apache.coyote.http11.response.HttpResponseGenerator; -import org.apache.coyote.http11.response.ResponseEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Http11Processor implements Runnable, Processor { - private static final Logger LOG = LoggerFactory.getLogger(Http11Processor.class); - private static final String INDEX_PAGE = "/index.html"; - private static final String REGISTER_PAGE = "/register.html"; - private static final String LOGIN_PAGE = "/login.html"; - private static final String ACCOUNT = "account"; - private static final String PASSWORD = "password"; - private static final String EMAIL = "email"; + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final HttpResponse INTERNAL_SERVER_ERROR_RESPONSE = new HttpResponse(HttpVersion.HTTP_1_1) + .setHttpStatus(HttpStatus.INTERNAL_SERVER_ERROR) + .sendRedirect("/500.html"); private final Socket connection; + private final Adapter adapter; + private final HttpRequestParser httpRequestParser = new HttpRequestParser(); private final HttpResponseGenerator httpResponseGenerator = new HttpResponseGenerator(); - private final SessionManager sessionManager = new SessionManager(); - public Http11Processor(final Socket connection) { + public Http11Processor(final Socket connection, final Adapter adapter) { this.connection = connection; + this.adapter = adapter; } @Override public void run() { - LOG.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); process(connection); } @@ -55,103 +45,28 @@ public void process(final Socket connection) { try (final InputStream inputStream = connection.getInputStream(); final OutputStream outputStream = connection.getOutputStream(); final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { - final String firstLine = bufferedReader.readLine(); - if (firstLine == null) { - return; - } - final RequestLine requestLine = RequestLine.from(firstLine); - final RequestHeader requestHeader = readHeader(bufferedReader); - final RequestBody requestBody = readBody(bufferedReader, requestHeader); - - final ResponseEntity responseEntity = handleRequest(requestLine, requestHeader, requestBody); - - final String response = httpResponseGenerator.generate(responseEntity); - outputStream.write(response.getBytes()); - outputStream.flush(); - } catch (IOException | UncheckedServletException e) { - LOG.error(e.getMessage(), e); - } - } - - private RequestHeader readHeader(final BufferedReader bufferedReader) throws IOException { - final StringBuilder stringBuilder = new StringBuilder(); - for (String line = bufferedReader.readLine(); !"".equals(line); line = bufferedReader.readLine()) { - stringBuilder.append(line).append(CRLF); - } - return RequestHeader.from(stringBuilder.toString()); - } - - private RequestBody readBody(final BufferedReader bufferedReader, final RequestHeader requestHeader) - throws IOException { - final String contentLength = requestHeader.get("Content-Length"); - if (contentLength == null) { - return RequestBody.empty(); + execute(bufferedReader, outputStream); + } catch (final IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); } - final int length = Integer.parseInt(contentLength); - char[] buffer = new char[length]; - bufferedReader.read(buffer, 0, length); - return RequestBody.from(new String(buffer)); } - private ResponseEntity handleRequest( - final RequestLine requestLine, - final RequestHeader requestHeader, - final RequestBody requestBody - ) { - final String path = requestLine.parseUriWithOutQueryString(); - if (path.equals("/login")) { - return login(requestLine, requestHeader, requestBody); - } - if (path.equals("/register")) { - return register(requestLine, requestBody); - } - return new ResponseEntity(HttpStatus.OK, path); - } + private void execute(final BufferedReader bufferedReader, final OutputStream outputStream) throws IOException { + try { + final HttpRequest httpRequest = httpRequestParser.parse(bufferedReader); + final HttpResponse httpResponse = new HttpResponse(httpRequest.getHttpVersion()); - private ResponseEntity login( - final RequestLine requestLine, - final RequestHeader requestHeader, - final RequestBody requestBody - ) { - if (requestLine.getHttpMethod() == HttpMethod.GET) { - final HttpCookie httpCookie = requestHeader.parseCookie(); - final Session session = sessionManager.findSession(httpCookie.getJSessionId()); - if (session != null) { - return new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); - } - return new ResponseEntity(HttpStatus.OK, LOGIN_PAGE); + adapter.service(httpRequest, httpResponse); + write(outputStream, httpResponse); + } catch (final Exception e) { + log.error(e.getMessage(), e); + write(outputStream, INTERNAL_SERVER_ERROR_RESPONSE); } - final String account = requestBody.get(ACCOUNT); - final String password = requestBody.get(PASSWORD); - return InMemoryUserRepository.findByAccount(account) - .filter(user -> user.checkPassword(password)) - .map(this::loginSuccess) - .orElseGet(() -> new ResponseEntity(HttpStatus.UNAUTHORIZED, "/401.html")); - } - - private ResponseEntity loginSuccess(final User user) { - final String uuid = UUID.randomUUID().toString(); - final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); - responseEntity.setJSessionId(uuid); - final Session session = new Session(uuid); - session.setAttribute("user", user); - sessionManager.add(session); - return responseEntity; } - private ResponseEntity register(final RequestLine requestLine, final RequestBody requestBody) { - if (requestLine.getHttpMethod() == HttpMethod.GET) { - return new ResponseEntity(HttpStatus.OK, REGISTER_PAGE); - } - final String account = requestBody.get(ACCOUNT); - - if (InMemoryUserRepository.findByAccount(account).isPresent()) { - return new ResponseEntity(HttpStatus.CONFLICT, "/409.html"); - } - - final String password = requestBody.get(PASSWORD); - final String email = requestBody.get(EMAIL); - InMemoryUserRepository.save(new User(account, password, email)); - return new ResponseEntity(HttpStatus.FOUND, INDEX_PAGE); + private void write(final OutputStream outputStream, final HttpResponse httpResponse) throws IOException { + final String generate = httpResponseGenerator.generate(httpResponse); + outputStream.write(generate.getBytes()); + outputStream.flush(); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java index be6350538c..ffabe48a83 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Constants.java @@ -4,6 +4,7 @@ public class Constants { public static final String CRLF = "\r\n"; public static final String BLANK = " "; + public static final String EMPTY = ""; private Constants() { } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Headers.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Headers.java new file mode 100644 index 0000000000..7b0515ff0c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Headers.java @@ -0,0 +1,54 @@ +package org.apache.coyote.http11.common; + +import static org.apache.coyote.http11.common.Constants.EMPTY; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class Headers { + + private static final String DELIMITER = ": "; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final String COOKIE_HEADER = "Cookie"; + + private final Map items = new HashMap<>(); + + public Headers() { + this(Collections.emptyMap()); + } + + private Headers(final Map items) { + this.items.putAll(items); + } + + public void addHeader(final String line) { + if (line == null || line.isBlank()) { + return; + } + final String[] header = line.split(DELIMITER); + items.put(header[KEY_INDEX].strip(), header[VALUE_INDEX].strip()); + } + + public void addHeader(final String key, final String value) { + items.put(key, value); + } + + public HttpCookie parseCookie() { + final String cookie = items.getOrDefault(COOKIE_HEADER, EMPTY); + return HttpCookie.from(cookie); + } + + public String get(final String key) { + return items.getOrDefault(key, EMPTY); + } + + public boolean isEmpty() { + return items.isEmpty(); + } + + public Map getItems() { + return items; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Http11Exception.java similarity index 76% rename from tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java rename to tomcat/src/main/java/org/apache/coyote/http11/common/Http11Exception.java index 1339c38abf..cda5599e41 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/exception/Http11Exception.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Http11Exception.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.exception; +package org.apache.coyote.http11.common; public class Http11Exception extends RuntimeException { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java index 779c9270c5..20bd3046bc 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java @@ -2,6 +2,7 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toMap; +import static org.apache.coyote.http11.common.Constants.EMPTY; import java.util.Arrays; import java.util.HashMap; @@ -41,11 +42,15 @@ public void put(final String key, final String value) { } public String get(final String key) { - return items.get(key); + return items.getOrDefault(key, EMPTY); } public String getJSessionId() { - return items.get(JSESSION_ID); + return items.getOrDefault(JSESSION_ID, EMPTY); + } + + public boolean isEmpty() { + return items.isEmpty(); } public Map getItems() { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java index b815f235e9..bfbfeab904 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpExtensionType.java @@ -3,10 +3,11 @@ import java.util.Arrays; public enum HttpExtensionType { - HTML(".html", "text/html"), - CSS(".css", "text/css"), - JS(".js", "text/javascript"), - ICO(".ico", "image/svg+xml"), + HTML(".html", "text/html;charset=utf-8 "), + CSS(".css", "text/css; "), + JS(".js", "text/javascript; "), + ICO(".ico", "image/svg+xml; "), + SVG(".svg", "text/html; "), ; private final String extension; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java index e57a21e81c..4b50a84315 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java @@ -1,7 +1,6 @@ package org.apache.coyote.http11.common; import java.util.Arrays; -import org.apache.coyote.http11.exception.InvalidHttpMethodException; public enum HttpMethod { GET, @@ -11,6 +10,6 @@ public static HttpMethod from(final String input) { return Arrays.stream(values()) .filter(value -> value.name().equals(input)) .findAny() - .orElseThrow(InvalidHttpMethodException::new); + .orElseThrow(() -> new Http11Exception("올바르지 않은 HttpMethod 형식입니다.")); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java index cabc6af05f..2f2c28bf94 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java @@ -3,8 +3,11 @@ public enum HttpStatus { OK("200"), FOUND("302"), + BAD_REQUEST("400"), UNAUTHORIZED("401"), - CONFLICT("409"); + NOT_FOUND("404"), + CONFLICT("409"), + INTERNAL_SERVER_ERROR("500"); private final String code; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpVersion.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpVersion.java new file mode 100644 index 0000000000..499090b57d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpVersion.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; + +/** + * HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT + * . + */ +public enum HttpVersion { + HTTP_1_0("HTTP/1.0"), + HTTP_1_1("HTTP/1.1"), + ; + + private final String field; + + HttpVersion(final String field) { + this.field = field; + } + + public static HttpVersion from(final String field) { + return Arrays.stream(values()) + .filter(httpVersion -> httpVersion.field.equals(field)) + .findAny() + .orElseThrow(() -> new Http11Exception("해당 Http Version을 지원할 수 없습니다.")); + } + + public String getField() { + return field; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java index 1cb2c12f7e..c67ef18408 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java @@ -1,15 +1,26 @@ package org.apache.coyote.http11.common; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.UUID; public class Session { private final String id; private final Map items = new HashMap<>(); + public Session() { + this(UUID.randomUUID().toString(), Collections.emptyMap()); + } + public Session(final String id) { + this(id, Collections.emptyMap()); + } + + public Session(final String id, Map attributes) { this.id = id; + this.items.putAll(attributes); } public Object getAttribute(final String key) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java deleted file mode 100644 index 08d276e37b..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.apache.coyote.http11.common; - -import java.util.HashMap; -import java.util.Map; - -public class SessionManager { - - private static final Map SESSIONS = new HashMap<>(); - - public void add(final Session session) { - SESSIONS.put(session.getId(), session); - } - - public Session findSession(final String id) { - return SESSIONS.get(id); - } - - public void remove(final String id) { - SESSIONS.remove(id); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java deleted file mode 100644 index c251e74e2b..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpMethodException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.coyote.http11.exception; - -public class InvalidHttpMethodException extends Http11Exception { - - public InvalidHttpMethodException() { - super("올바르지 않은 HttpMethod 형식입니다."); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java deleted file mode 100644 index 5b30a82b9f..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidRequestLineException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.apache.coyote.http11.exception; - -public class InvalidRequestLineException extends Http11Exception { - - public InvalidRequestLineException() { - super("올바르지 않은 RequestLine 형식입니다."); - } -} 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..b29e3b7437 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,66 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.common.Session; + +public class HttpRequest { + + private final RequestLine requestLine; + private final Headers headers; + private final RequestBody requestBody; + private Session session; + + public HttpRequest( + final RequestLine requestLine, + final Headers requestHeader, + final RequestBody requestBody + ) { + this.requestLine = requestLine; + this.headers = requestHeader; + this.requestBody = requestBody; + } + + public boolean isGet() { + return requestLine.getHttpMethod() == HttpMethod.GET; + } + + public boolean isPost() { + return requestLine.getHttpMethod() == HttpMethod.POST; + } + + public HttpCookie parseCookie() { + return headers.parseCookie(); + } + + public void setSession(final Session session) { + this.session = session; + } + + public String parseSessionId() { + final HttpCookie httpCookie = headers.parseCookie(); + return httpCookie.getJSessionId(); + } + + public HttpVersion getHttpVersion() { + return requestLine.getHttpVersion(); + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public Headers getHeaders() { + return headers; + } + + public RequestBody getRequestBody() { + return requestBody; + } + + public Session getSession() { + return session; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java new file mode 100644 index 0000000000..988b675630 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java @@ -0,0 +1,39 @@ +package org.apache.coyote.http11.request; + +import static org.apache.coyote.http11.common.Constants.EMPTY; + +import java.io.BufferedReader; +import java.io.IOException; +import org.apache.coyote.http11.common.Headers; + +public class HttpRequestParser { + + public HttpRequest parse(final BufferedReader bufferedReader) throws IOException { + final RequestLine requestLine = RequestLine.from(bufferedReader.readLine()); + final Headers requestHeader = parseRequestHeader(bufferedReader); + final RequestBody requestBody = parseRequestBody(bufferedReader, requestHeader); + return new HttpRequest(requestLine, requestHeader, requestBody); + } + + private Headers parseRequestHeader(final BufferedReader bufferedReader) throws IOException { + final Headers headers = new Headers(); + for (String line = bufferedReader.readLine(); !EMPTY.equals(line); line = bufferedReader.readLine()) { + headers.addHeader(line); + } + return headers; + } + + private RequestBody parseRequestBody( + final BufferedReader bufferedReader, + final Headers headers + ) throws IOException { + final String contentLength = headers.get("Content-Length"); + if (contentLength.isEmpty()) { + return new RequestBody(); + } + final int length = Integer.parseInt(contentLength); + char[] buffer = new char[length]; + bufferedReader.read(buffer, 0, length); + return RequestBody.from(new String(buffer)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Path.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Path.java new file mode 100644 index 0000000000..7f049931dd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Path.java @@ -0,0 +1,29 @@ +package org.apache.coyote.http11.request; + +public class Path { + + private static final String QUERY_STRING_BEGIN = "?"; + private static final int EMPTY_QUERY_STRING = -1; + + private final String value; + + private Path(final String value) { + this.value = value; + } + + public static Path from(final String path) { + return new Path(path); + } + + public String parseUri() { + final int queryStringIndex = value.indexOf(QUERY_STRING_BEGIN); + if (queryStringIndex == EMPTY_QUERY_STRING) { + return value; + } + return value.substring(0, queryStringIndex); + } + + public QueryString parseQueryString() { + return QueryString.from(value); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java index ad50c1713a..c1f5240f50 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java @@ -1,5 +1,6 @@ package org.apache.coyote.http11.request; +import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toMap; import java.util.Arrays; @@ -17,18 +18,14 @@ public class QueryString { private final Map items = new HashMap<>(); - private QueryString() { - this(Map.of()); - } - - public QueryString(final Map items) { + private QueryString(final Map items) { this.items.putAll(items); } public static QueryString from(final String uri) { int queryStringIndex = uri.indexOf(QUERY_STRING_BEGIN); if (queryStringIndex == EMPTY) { - return new QueryString(); + return new QueryString(emptyMap()); } return new QueryString(parseQueryString(uri, queryStringIndex)); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java index ec779872aa..050cabf734 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java @@ -2,8 +2,10 @@ import static java.util.stream.Collectors.collectingAndThen; import static java.util.stream.Collectors.toMap; +import static org.apache.coyote.http11.common.Constants.EMPTY; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -13,34 +15,29 @@ public class RequestBody { private static final String DELIMITER = "="; private static final int KEY_INDEX = 0; private static final int VALUE_INDEX = 1; + private static final int FIELD_COUNT = 2; private final Map items = new HashMap<>(); - private RequestBody() { + public RequestBody() { + this(Collections.emptyMap()); } private RequestBody(final Map items) { this.items.putAll(items); } - public static RequestBody empty() { - return new RequestBody(); - } - public static RequestBody from(final String body) { - if (body.isEmpty()) { - return new RequestBody(); - } return Arrays.stream(body.split(SEPARATOR)) - .map(field -> field.split(DELIMITER)) + .map(field -> field.split(DELIMITER, FIELD_COUNT)) .collect(collectingAndThen( - toMap(field -> field[KEY_INDEX], field -> field[VALUE_INDEX]), + toMap(field -> field[KEY_INDEX].strip(), field -> field[VALUE_INDEX].strip()), RequestBody::new )); } public String get(final String key) { - return items.get(key); + return items.getOrDefault(key, EMPTY); } public Map getItems() { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java deleted file mode 100644 index 44f4c78c71..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.apache.coyote.http11.request; - -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toMap; -import static org.apache.coyote.http11.common.Constants.CRLF; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.apache.coyote.http11.common.HttpCookie; - -public class RequestHeader { - - private static final String DELIMITER = ": "; - private static final int KEY_INDEX = 0; - private static final int VALUE_INDEX = 1; - private static final String COOKIE_HEADER = "Cookie"; - - private final Map items = new HashMap<>(); - - private RequestHeader(final Map items) { - this.items.putAll(items); - } - - public static RequestHeader from(final String headers) { - return Arrays.stream(headers.split(CRLF)) - .map(header -> header.split(DELIMITER)) - .collect(collectingAndThen( - toMap(header -> header[KEY_INDEX], header -> header[VALUE_INDEX]), - RequestHeader::new - )); - } - - public HttpCookie parseCookie() { - final String cookie = items.getOrDefault(COOKIE_HEADER, ""); - return HttpCookie.from(cookie); - } - - public String get(final String key) { - return items.get(key); - } - - public Map getItems() { - return items; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java index 0e0b152270..7a95766266 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -1,7 +1,8 @@ package org.apache.coyote.http11.request; +import org.apache.coyote.http11.common.Http11Exception; import org.apache.coyote.http11.common.HttpMethod; -import org.apache.coyote.http11.exception.InvalidRequestLineException; +import org.apache.coyote.http11.common.HttpVersion; public class RequestLine { @@ -10,16 +11,14 @@ public class RequestLine { private static final int HTTP_VERSION_INDEX = 2; private static final String DELIMITER = " "; private static final int VALID_REQUEST_LINE_SIZE = 3; - private static final String QUERY_STRING_BEGIN = "?"; - private static final int EMPTY_QUERY_STRING = -1; private final HttpMethod httpMethod; - private final String uri; - private final String httpVersion; + private final Path path; + private final HttpVersion httpVersion; - private RequestLine(final HttpMethod httpMethod, final String uri, final String httpVersion) { + private RequestLine(final HttpMethod httpMethod, final Path path, final HttpVersion httpVersion) { this.httpMethod = httpMethod; - this.uri = uri; + this.path = path; this.httpVersion = httpVersion; } @@ -28,38 +27,34 @@ public static RequestLine from(final String line) { validate(requestLine); return new RequestLine( HttpMethod.from(requestLine[HTTP_METHOD_INDEX]), - requestLine[URI_INDEX], - requestLine[HTTP_VERSION_INDEX] + Path.from(requestLine[URI_INDEX]), + HttpVersion.from(requestLine[HTTP_VERSION_INDEX]) ); } private static void validate(final String[] requestLine) { if (requestLine.length != VALID_REQUEST_LINE_SIZE) { - throw new InvalidRequestLineException(); + throw new Http11Exception("올바르지 않은 RequestLine 형식입니다."); } } - public String parseUriWithOutQueryString() { - final int queryStringIndex = uri.indexOf(QUERY_STRING_BEGIN); - if (queryStringIndex == EMPTY_QUERY_STRING) { - return uri; - } - return uri.substring(0, queryStringIndex); + public String parseUri() { + return path.parseUri(); } public QueryString parseQueryString() { - return QueryString.from(uri); + return path.parseQueryString(); } public HttpMethod getHttpMethod() { return httpMethod; } - public String getUri() { - return uri; + public Path getPath() { + return path; } - public String getHttpVersion() { + public HttpVersion getHttpVersion() { return httpVersion; } } 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..310ff58d6c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,70 @@ +package org.apache.coyote.http11.response; + +import org.apache.coyote.http11.common.Session; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; + +public class HttpResponse { + + private final HttpVersion httpVersion; + private final Headers headers = new Headers(); + private final HttpCookie httpCookie = new HttpCookie(); + private HttpStatus httpStatus; + private Session session; + private String redirect; + + public HttpResponse(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + } + + public HttpResponse addHeader(final String key, final String value) { + headers.addHeader(key, value); + return this; + } + + public HttpResponse setHttpStatus(final HttpStatus httpStatus) { + this.httpStatus = httpStatus; + return this; + } + + public HttpResponse sendRedirect(final String url) { + this.redirect = url; + return this; + } + + public HttpResponse setSession(final Session session) { + this.session = session; + return this; + } + + public HttpResponse setCookie(final String key, final String value) { + httpCookie.put(key, value); + return this; + } + + public HttpVersion getHttpVersion() { + return httpVersion; + } + + public Headers getHeaders() { + return headers; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String getRedirect() { + return redirect; + } + + public HttpCookie getHttpCookie() { + return httpCookie; + } + + public Session getSession() { + return session; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java index 8423da2bbe..1fad3a7982 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseGenerator.java @@ -2,75 +2,87 @@ import static org.apache.coyote.http11.common.Constants.BLANK; import static org.apache.coyote.http11.common.Constants.CRLF; +import static org.apache.coyote.http11.common.Constants.EMPTY; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.util.stream.Collectors; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpCookie; import org.apache.coyote.http11.common.HttpExtensionType; import org.apache.coyote.http11.common.HttpStatus; public class HttpResponseGenerator { + private static final String STATIC_FOLDER_PATH = "static"; private final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - public String generate(final ResponseEntity responseEntity) throws IOException { - final String uri = responseEntity.getUri(); - if (uri.equals("/")) { - return generateResponse(responseEntity, "Hello world!"); - } - if (responseEntity.getHttpStatus() == HttpStatus.FOUND) { - return generateRedirectResponse(responseEntity); - } - final String responseBody = readStaticFile(uri); - return generateResponse(responseEntity, responseBody); - } - - private String generateResponse(final ResponseEntity responseEntity, final String responseBody) { + public String generate(final HttpResponse httpResponse) throws IOException { + final String responseBody = readStaticFile(httpResponse.getRedirect()); return String.join( CRLF, - generateHttpStatusLine(responseEntity.getHttpStatus()), - generateContentTypeLine(responseEntity.getHttpExtensionType()), - generateContentLengthLine(responseBody), - "", + generateHttpStatusLine(httpResponse), + generateHttpHeadersLine(httpResponse, responseBody), + EMPTY, responseBody ); } - private String generateHttpStatusLine(final HttpStatus httpStatus) { - return String.join(BLANK, "HTTP/1.1", httpStatus.getCode(), httpStatus.name(), ""); + private String readStaticFile(final String uri) throws IOException { + final URL resource = classLoader.getResource(STATIC_FOLDER_PATH + uri); + final File file = new File(resource.getFile()); + return new String(Files.readAllBytes(file.toPath())); + } + + private String generateHttpStatusLine(final HttpResponse httpResponse) { + final HttpStatus httpStatus = httpResponse.getHttpStatus(); + return String.join( + BLANK, + httpResponse.getHttpVersion().getField(), + httpStatus.getCode(), + httpStatus.name(), + EMPTY + ); + } + + private String generateHttpHeadersLine(final HttpResponse httpResponse, final String responseBody) { + final StringBuilder stringBuilder = new StringBuilder() + .append(generateContentTypeLine(HttpExtensionType.from(httpResponse.getRedirect()))) + .append(CRLF) + .append(generateContentLengthLine(responseBody)); + + final Headers headers = httpResponse.getHeaders(); + if (!headers.isEmpty()) { + stringBuilder.append(CRLF).append(generateHeaderLine(headers)); + } + + final HttpCookie httpCookie = httpResponse.getHttpCookie(); + if (!httpCookie.isEmpty()) { + stringBuilder.append(CRLF).append(generateCookieLine(httpCookie)); + } + + return stringBuilder.toString(); } private String generateContentTypeLine(final HttpExtensionType httpExtensionType) { - return "Content-Type: " + httpExtensionType.getContentType() + ";charset=utf-8 "; + return "Content-Type: " + httpExtensionType.getContentType(); } private CharSequence generateContentLengthLine(final String responseBody) { return "Content-Length: " + responseBody.getBytes().length + BLANK; } - private String readStaticFile(final String uri) throws IOException { - final URL resource = classLoader.getResource("static" + uri); - final File file = new File(resource.getFile()); - return new String(Files.readAllBytes(file.toPath())); + private String generateHeaderLine(final Headers headers) { + return headers.getItems().entrySet().stream() + .map(header -> header.getKey() + ": " + header.getValue()) + .collect(Collectors.joining(CRLF)); } - private String generateRedirectResponse(final ResponseEntity responseEntity) { - final HttpStatus httpStatus = responseEntity.getHttpStatus(); - final String firstLine = String.join(BLANK, "HTTP/1.1", httpStatus.getCode(), httpStatus.name(), ""); - return String.join( - CRLF, - firstLine, - "Location: " + responseEntity.getUri(), - generateSetCookieLine(responseEntity) - ); - } - - private String generateSetCookieLine(final ResponseEntity responseEntity) { - final String jsessionid = responseEntity.getHttpCookie().get("JSESSIONID"); - if (jsessionid == null) { - return ""; - } - return "Set-Cookie: JSESSIONID=" + jsessionid + " "; + private String generateCookieLine(final HttpCookie httpCookie) { + return "Set-Cookie: " + httpCookie.getItems().entrySet().stream() + .map(cookie -> cookie.getKey() + "=" + cookie.getValue() + ";") + .collect(Collectors.joining(BLANK)); } } diff --git a/tomcat/src/main/resources/static/400.html b/tomcat/src/main/resources/static/400.html new file mode 100644 index 0000000000..2828ac208a --- /dev/null +++ b/tomcat/src/main/resources/static/400.html @@ -0,0 +1,52 @@ + + + + + + + + + 404 Error - SB Admin + + + + +
+
+
+
+
+
+
+

400

+

Bad Request

+

Access to this resource is denied.

+ + + Return to Dashboard + +
+
+
+
+
+
+ +
+ + + + diff --git a/tomcat/src/main/resources/static/home.html b/tomcat/src/main/resources/static/home.html new file mode 100644 index 0000000000..cd0875583a --- /dev/null +++ b/tomcat/src/main/resources/static/home.html @@ -0,0 +1 @@ +Hello world! diff --git a/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java new file mode 100644 index 0000000000..bdec91e5bf --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java @@ -0,0 +1,40 @@ +package nextstep.jwp.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.catalina.controller.Controller; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HomeControllerTest { + + private final Controller controller = new HomeController(); + + @Test + void GET_요청을_하는_경우_OK_응답을_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("GET / HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/home.html") + ); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java new file mode 100644 index 0000000000..1220d75b83 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java @@ -0,0 +1,117 @@ +package nextstep.jwp.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.UUID; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.SessionManager; +import org.apache.catalina.controller.Controller; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.common.Session; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LoginControllerTest { + + private final Controller controller = new LoginController(); + private final SessionManager sessionManager = new SessionManager(); + + @BeforeEach + void setUp() { + sessionManager.clear(); + InMemoryUserRepository.clear(); + } + + @Test + void GET_요청을_받았을_때_세션에_사용자_정보가_없는_경우_login_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /login HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + httpRequest.setSession(new Session()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/login.html") + ); + } + + @Test + void GET_요청을_받았을_때_세션에_사용자_정보가_있는_경우_index_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /login HTTP/1.1"); + final String uuid = UUID.randomUUID().toString(); + final Session session = new Session(uuid); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + session.setAttribute("user", new User("gugu", "password", "gugu@naver.com")); + httpRequest.setSession(session); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + sessionManager.add(session); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.FOUND), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/index.html") + ); + } + + @Test + void POST_요청을_받았을_때_인증이_실패하는_경우_401_UNAUTHORIZED를_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("POST /login HTTP/1.1"); + final RequestBody requestBody = RequestBody.from("account=hello&password=pw"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), requestBody); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + InMemoryUserRepository.save(new User("hello", "world", "email@email.com")); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.UNAUTHORIZED), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/401.html") + ); + } + + @Test + void POST_요청을_받았을_때_인증에_성공하는_경우_index_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("POST /login HTTP/1.1"); + final RequestBody requestBody = RequestBody.from("account=hello&password=world"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), requestBody); + final String uuid = UUID.randomUUID().toString(); + final Session session = new Session(uuid); + httpRequest.setSession(session); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + InMemoryUserRepository.save(new User("hello", "world", "email@email.com")); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.FOUND), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/index.html") + ); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java new file mode 100644 index 0000000000..7953e62505 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java @@ -0,0 +1,82 @@ +package nextstep.jwp.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import nextstep.jwp.db.InMemoryUserRepository; +import org.apache.catalina.controller.Controller; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RegisterControllerTest { + + private final Controller controller = new RegisterController(); + + @BeforeEach + void setUp() { + InMemoryUserRepository.clear(); + } + + @Test + void GET_요청을_시_회원가입_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /register HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/register.html") + ); + } + + @Test + void POST_요청_시_이미_존재하는_회원인_경우_회원가입_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /register HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/register.html") + ); + } + + @Test + void POST_요청_시_회원가입에_성공하는_경우_index_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("POST /register HTTP/1.1"); + final RequestBody requestBody = RequestBody.from("account=gugu&password=password&email=hkkang@woowahan.com"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), requestBody); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.FOUND), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/index.html") + ); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/model/UserTest.java b/tomcat/src/test/java/nextstep/jwp/model/UserTest.java new file mode 100644 index 0000000000..76ce2960ea --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/model/UserTest.java @@ -0,0 +1,22 @@ +package nextstep.jwp.model; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UserTest { + + @Test + void 사용자를_생성할_때_빈_정보를_입력하는_경우_예외를_던진다() { + // expect + assertThatThrownBy(() -> new User("", "password", "email")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("올바른 사용자 정보를 입력해주세요."); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java b/tomcat/src/test/java/org/apache/catalina/SessionManagerTest.java similarity index 67% rename from tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java rename to tomcat/src/test/java/org/apache/catalina/SessionManagerTest.java index 23c59319eb..0843cb5f27 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/common/SessionManagerTest.java +++ b/tomcat/src/test/java/org/apache/catalina/SessionManagerTest.java @@ -1,7 +1,9 @@ -package org.apache.coyote.http11.common; +package org.apache.catalina; import static org.assertj.core.api.Assertions.assertThat; +import org.apache.coyote.http11.common.Session; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -12,6 +14,11 @@ class SessionManagerTest { private final SessionManager sessionManager = new SessionManager(); + @BeforeEach + void setUp() { + sessionManager.clear(); + } + @Test void 세션을_추가한다() { // given @@ -47,6 +54,20 @@ class SessionManagerTest { sessionManager.remove("helloworld"); // then - assertThat(sessionManager.findSession("helloworld")).isNull(); + final Session findSession = sessionManager.findSession("helloworld"); + assertThat(findSession).isNotEqualTo(session); + } + + @Test + void 세션을_초기화한다() { + // given + final Session session = new Session("helloworld"); + sessionManager.add(session); + + // when + sessionManager.clear(); + + // then + assertThat(session.getItems()).isEmpty(); } } diff --git a/tomcat/src/test/java/org/apache/catalina/controller/StaticControllerTest.java b/tomcat/src/test/java/org/apache/catalina/controller/StaticControllerTest.java new file mode 100644 index 0000000000..260fa4b9b5 --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/controller/StaticControllerTest.java @@ -0,0 +1,58 @@ +package org.apache.catalina.controller; + +import static org.apache.coyote.http11.common.HttpStatus.NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.RequestBody; +import org.apache.coyote.http11.request.RequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class +StaticControllerTest { + + private final Controller controller = new StaticController(); + + @Test + void Get_요청시_정적_파일을_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /index.html HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/index.html") + ); + } + + @Test + void Get_요청시_uri에_해당하는_파일이_없는경우_404_NOT_FOUND_페이지를_반환하도록_설정한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /helloworld.html HTTP/1.1"); + final HttpRequest httpRequest = new HttpRequest(requestLine, new Headers(), new RequestBody()); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + controller.service(httpRequest, httpResponse); + + // then + assertAll( + () -> assertThat(httpResponse.getHttpStatus()).isEqualTo(NOT_FOUND), + () -> assertThat(httpResponse.getRedirect()).isEqualTo("/404.html") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java index d221405d2d..ab848a092d 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -6,9 +6,12 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.util.Collections; +import org.apache.catalina.RequestAdapter; +import org.apache.catalina.RequestMapper; +import org.apache.catalina.controller.StaticController; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import support.StubSocket; @@ -16,26 +19,6 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class Http11ProcessorTest { - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final String expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - @Test void index() throws IOException { // given @@ -47,7 +30,8 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final RequestAdapter requestAdapter = new RequestAdapter(new RequestMapper(Collections.emptyMap())); + final Http11Processor processor = new Http11Processor(socket, requestAdapter); // when processor.process(socket); @@ -62,168 +46,4 @@ void index() throws IOException { assertThat(socket.output()).isEqualTo(expected); } - - @Nested - class 로그인 { - - @Test - void 페이지_접속시_200_OK_를_반환한다() 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"); - final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - final String expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: " + responseBody.getBytes().length + " \r\n" + - "\r\n" + - responseBody; - - assertThat(socket.output()).isEqualTo(expected); - } - - @Test - void 성공_시_302_FOUND를_반환한다() { - // given - final String content = "account=gugu&password=password"; - final String httpRequest = String.join("\r\n", - "POST /login HTTP/1.1 ", - "Host: localhost:8080 ", - "Content-Length: " + content.getBytes().length, - "", - content); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final String expected = "HTTP/1.1 302 FOUND \r\nLocation: /index.html"; - assertThat(socket.output()).contains(expected); - } - - @Test - void 실패_시_401_UNAUTHORIZE를_반환한다() throws IOException { - // given - final String content = "account=gugu&password=password2"; - final String httpRequest = String.join("\r\n", - "POST /login HTTP/1.1 ", - "Host: localhost:8080 ", - "Content-Length: " + content.getBytes().length, - "", - content); - - 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"); - final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - final String expected = "HTTP/1.1 401 UNAUTHORIZED \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: " + responseBody.getBytes().length + " \r\n" + - "\r\n" + - responseBody; - assertThat(socket.output()).isEqualTo(expected); - } - } - - @Nested - class 회원가입 { - - @Test - void 페이지_접속시_200_OK_를_반환한다() 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"); - final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - final String expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: " + responseBody.getBytes().length + " \r\n" + - "\r\n" + - responseBody; - - assertThat(socket.output()).isEqualTo(expected); - } - - @Test - void 성공_시_302_FOUND를_반환한다() { - // given - final String content = "account=gugu2&password=password&email=hkkang@woowahan.com"; - final String httpRequest = String.join("\r\n", - "POST /register HTTP/1.1 ", - "Host: localhost:8080 ", - "Content-Length: " + content.getBytes().length, - "", - content); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final String expected = "HTTP/1.1 302 FOUND \r\nLocation: /index.html"; - assertThat(socket.output()).contains(expected); - } - - @Test - void 닉네임_중복시_409_CONFLICT를_반환한다() throws IOException { - // given - final String content = "account=gugu&password=password&email=hkkang@woowahan.com"; - final String httpRequest = String.join("\r\n", - "POST /register HTTP/1.1 ", - "Host: localhost:8080 ", - "Content-Length: " + content.getBytes().length, - "", - content); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final URL resource = getClass().getClassLoader().getResource("static/409.html"); - final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - final String expected = "HTTP/1.1 409 CONFLICT \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: " + responseBody.getBytes().length + " \r\n" + - "\r\n" + - responseBody; - assertThat(socket.output()).isEqualTo(expected); - } - } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HeadersTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HeadersTest.java new file mode 100644 index 0000000000..dc4b1b01dc --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HeadersTest.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HeadersTest { + + @Test + void 헤더값을_추가한다() { + // given + final Headers headers = new Headers(); + + // when + headers.addHeader("Connection: keep-alive"); + headers.addHeader("Sec-Fetch-Dest: image"); + + // then + assertThat(headers.getItems()).contains( + entry("Connection", "keep-alive"), + entry("Sec-Fetch-Dest", "image") + ); + } + + @Test + void 헤더값을_Key_Value_형식으로_추가한다() { + // given + final Headers headers = new Headers(); + + // when + headers.addHeader("Connection", "keep-alive"); + headers.addHeader("Sec-Fetch-Dest", "image"); + + // then + assertThat(headers.getItems()).contains( + entry("Connection", "keep-alive"), + entry("Sec-Fetch-Dest", "image") + ); + } + + @Test + void 쿠키값을_파싱하여_반환한다() { + // given + final Headers headers = new Headers(); + headers.addHeader("Cookie: yummy_cookie=choco; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46"); + + // when + final HttpCookie httpCookie = headers.parseCookie(); + + // then + assertThat(httpCookie.getItems()).contains( + entry("yummy_cookie", "choco"), + entry("JSESSIONID", "656cef62-e3c4-40bc-a8df-94732920ed46") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java index e0b24f04b6..4ebd6ca92e 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpCookieTest.java @@ -60,4 +60,13 @@ class HttpCookieTest { // expect assertThat(cookie.getJSessionId()).isEqualTo(uuid); } + + @Test + void cookie가_비어있는지_확인한다() { + // given + final HttpCookie cookie = new HttpCookie(); + + // expect + assertThat(cookie.isEmpty()).isTrue(); + } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java index eeb80d192f..fd70394b98 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpMethodTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; -import org.apache.coyote.http11.exception.InvalidHttpMethodException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -18,7 +17,7 @@ class HttpMethodTest { // expect assertThatThrownBy(() -> HttpMethod.from(http)) - .isInstanceOf(InvalidHttpMethodException.class) + .isInstanceOf(Http11Exception.class) .hasMessage("올바르지 않은 HttpMethod 형식입니다."); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/HttpVersionTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpVersionTest.java new file mode 100644 index 0000000000..e34a97f147 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/HttpVersionTest.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.common; + +import static org.apache.coyote.http11.common.HttpVersion.from; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpVersionTest { + + @Test + void HttpVersion_문자열에_해당하는_HttpVersion이_없는_경우_예외를_던진다() { + // given + final String version = "HTTP/5.5"; + + // expect + assertThatThrownBy(() -> HttpVersion.from(version)) + .isInstanceOf(Http11Exception.class) + .hasMessage("해당 Http Version을 지원할 수 없습니다."); + } + + @Test + void HttpVersion_문자열을_입력받아_HttpVersion을_반환한다() { + // given + final String version = "HTTP/1.1"; + + // when + final HttpVersion httpVersion = from(version); + + // then + assertThat(httpVersion).isEqualTo(HttpVersion.HTTP_1_1); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java new file mode 100644 index 0000000000..ac947a4da3 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java @@ -0,0 +1,62 @@ +package org.apache.coyote.http11.request; + +import static org.apache.coyote.http11.common.Constants.CRLF; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpMethod; +import org.apache.coyote.http11.common.HttpVersion; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestParserTest { + + private final HttpRequestParser httpRequestParser = new HttpRequestParser(); + + @Test + void Http_요청을_입력받아_HttpRequest를_반환한다() throws IOException { + // given + final String content = "account=gugu&password=password&email=hkkang@woowahan.com"; + final String input = String.join( + CRLF, + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Length: " + content.getBytes().length, + "", + content + ); + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(input.getBytes()); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(byteArrayInputStream)); + + // when + final HttpRequest httpRequest = httpRequestParser.parse(bufferedReader); + + // then + final RequestLine requestLine = httpRequest.getRequestLine(); + final Headers headers = httpRequest.getHeaders(); + final RequestBody requestBody = httpRequest.getRequestBody(); + assertAll( + () -> assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.POST), + () -> assertThat(requestLine.parseUri()).isEqualTo("/register"), + () -> assertThat(requestLine.getHttpVersion()).isEqualTo(HttpVersion.HTTP_1_1), + () -> assertThat(headers.getItems()).contains( + entry("Host", "localhost:8080"), + entry("Content-Length", String.valueOf(content.getBytes().length)) + ), + () -> assertThat(requestBody.getItems()).contains( + entry("account", "gugu"), + entry("password", "password"), + entry("email", "hkkang@woowahan.com") + ) + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..010bc4da64 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,83 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.apache.coyote.http11.common.Headers; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpVersion; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestTest { + + @Test + void isGet_메서드는_GET_요청인_경우_true를_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /index.html HTTP/1.1 "); + final HttpRequest httpRequest = new HttpRequest(requestLine, null, null); + + // expect + assertThat(httpRequest.isGet()).isTrue(); + } + + @Test + void isGet_메서드는_GET_요청이_아닌_경우_false를_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("POST /register HTTP/1.1 "); + final HttpRequest httpRequest = new HttpRequest(requestLine, null, null); + + // expect + assertThat(httpRequest.isGet()).isFalse(); + } + + @Test + void isPost_메서드는_Post_요청인_경우_true를_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("POST /register HTTP/1.1 "); + final HttpRequest httpRequest = new HttpRequest(requestLine, null, null); + + // expect + assertThat(httpRequest.isPost()).isTrue(); + } + + @Test + void isPost_메서드는_Post_요청이_아닌_경우_true를_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /index.html HTTP/1.1 "); + final HttpRequest httpRequest = new HttpRequest(requestLine, null, null); + + // expect + assertThat(httpRequest.isPost()).isFalse(); + } + + @Test + void Cookie를_반환한다() { + // given + final Headers headers = new Headers(); + headers.addHeader("Cookie: yummy_cookie=choco; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46"); + final HttpRequest httpRequest = new HttpRequest(null, headers, null); + + // when + final HttpCookie httpCookie = httpRequest.parseCookie(); + + // then + assertThat(httpCookie.getItems()).contains( + entry("yummy_cookie", "choco"), + entry("JSESSIONID", "656cef62-e3c4-40bc-a8df-94732920ed46") + ); + } + + @Test + void httpVersion을_반환한다() { + // given + final RequestLine requestLine = RequestLine.from("GET /index.html HTTP/1.1 "); + final HttpRequest httpRequest = new HttpRequest(requestLine, null, null); + + // expect + assertThat(httpRequest.getHttpVersion()).isEqualTo(HttpVersion.HTTP_1_1); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/PathTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/PathTest.java new file mode 100644 index 0000000000..53baf5b89e --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/PathTest.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PathTest { + + @Test + void uri를_반환한다() { + // given + final Path path = Path.from("/login?account=gugu&password=password"); + + // expect + assertThat(path.parseUri()).isEqualTo("/login"); + } + + @Test + void queryString을_반환한다() { + // given + final Path path = Path.from("/login?account=gugu&password=password"); + + // when + final QueryString queryString = path.parseQueryString(); + + // then + assertThat(queryString.getItems()).contains( + entry("account", "gugu"), + entry("password", "password") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java index bc6ae51778..f7fb3583d0 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestBodyTest.java @@ -28,10 +28,19 @@ class RequestBodyTest { } @Test - void 빈_RequestBody를_반환한다() { - // expect - final RequestBody requestBody = RequestBody.empty(); - assertThat(requestBody.getItems()).isEmpty(); + void body_문자열을_입력받을_때_빈_값이_들어오는_경우_빈_문자열이_들어간다() { + // given + final String body = "account=gugu&password=&email=hkkang@woowahan.com"; + + // when + final RequestBody requestBody = RequestBody.from(body); + + // then + assertThat(requestBody.getItems()).contains( + entry("account", "gugu"), + entry("password", ""), + entry("email", "hkkang@woowahan.com") + ); } @Test diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java deleted file mode 100644 index 506d4bb724..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestHeaderTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.apache.coyote.http11.request; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -import org.apache.coyote.http11.common.HttpCookie; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class RequestHeaderTest { - - @Test - void header_문자열을_입력받아_RequestHeader를_반환한다() { - // given - final String header = "Connection: keep-alive\r\nSec-Fetch-Dest: image\r\n"; - - // when - final RequestHeader requestHeader = RequestHeader.from(header); - - // then - assertThat(requestHeader.getItems()).contains( - entry("Connection", "keep-alive"), - entry("Sec-Fetch-Dest", "image") - ); - } - - @Test - void 쿠키값을_파싱하여_반환한다() { - // given - final String header = "Cookie: yummy_cookie=choco; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46\r\n" - + "Sec-Fetch-Dest: image\r\n"; - final RequestHeader requestHeader = RequestHeader.from(header); - - // when - final HttpCookie httpCookie = requestHeader.parseCookie(); - - // then - assertThat(httpCookie.getItems()).contains( - entry("yummy_cookie", "choco"), - entry("JSESSIONID", "656cef62-e3c4-40bc-a8df-94732920ed46") - ); - } -} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java index 0798199e60..fc874d5db0 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java @@ -1,12 +1,13 @@ package org.apache.coyote.http11.request; +import static org.apache.coyote.http11.common.HttpVersion.HTTP_1_1; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.junit.jupiter.api.Assertions.assertAll; +import org.apache.coyote.http11.common.Http11Exception; import org.apache.coyote.http11.common.HttpMethod; -import org.apache.coyote.http11.exception.InvalidRequestLineException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ class RequestLineTest { // expect assertThatThrownBy(() -> RequestLine.from(line)) - .isInstanceOf(InvalidRequestLineException.class) + .isInstanceOf(Http11Exception.class) .hasMessage("올바르지 않은 RequestLine 형식입니다."); } @@ -37,8 +38,8 @@ class RequestLineTest { // then assertAll( () -> assertThat(requestLine.getHttpMethod()).isEqualTo(HttpMethod.GET), - () -> assertThat(requestLine.getUri()).isEqualTo("/index.html"), - () -> assertThat(requestLine.getHttpVersion()).isEqualTo("HTTP/1.1") + () -> assertThat(requestLine.parseUri()).isEqualTo("/index.html"), + () -> assertThat(requestLine.getHttpVersion()).isEqualTo(HTTP_1_1) ); } @@ -49,7 +50,7 @@ class RequestLineTest { final RequestLine requestLine = RequestLine.from(line); // when - final String result = requestLine.parseUriWithOutQueryString(); + final String result = requestLine.parseUri(); // then assertThat(result).isEqualTo("/login"); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java index 409e67bea8..8aabf4c28e 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseGeneratorTest.java @@ -7,6 +7,7 @@ import java.net.URL; import java.nio.file.Files; import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -17,53 +18,15 @@ class HttpResponseGeneratorTest { private final HttpResponseGenerator httpResponseGenerator = new HttpResponseGenerator(); - @Test - void 입력받은_ResponseEntity의_uri가_루트인_경우_Hello_world가_담긴_HttpResponse를_반환한다() throws IOException { - // given - final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.OK, "/"); - - // when - final String actual = httpResponseGenerator.generate(responseEntity); - - // then - final String expected = String.join( - "\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!" - ); - assertThat(actual).isEqualTo(expected); - } - - @Test - void 입력받은_HttpStatus에_따라_HttpResponse의_상태가_달라진다() throws IOException { - // given - final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.UNAUTHORIZED, "/"); - - // when - final String actual = httpResponseGenerator.generate(responseEntity); - - // then - final String expected = String.join( - "\r\n", - "HTTP/1.1 401 UNAUTHORIZED ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!" - ); - assertThat(actual).isEqualTo(expected); - } - @Test void 입력받은_uri에_해당하는_파일을_읽어와_HttpResponse를_반환한다() throws IOException { // given - final ResponseEntity responseEntity = new ResponseEntity(HttpStatus.OK, "/index.html"); + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1) + .setHttpStatus(HttpStatus.OK) + .sendRedirect("/index.html"); // when - final String actual = httpResponseGenerator.generate(responseEntity); + final String actual = httpResponseGenerator.generate(httpResponse); // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); @@ -72,7 +35,6 @@ class HttpResponseGeneratorTest { "Content-Length: 5564 \r\n" + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - assertThat(actual).isEqualTo(expected); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java new file mode 100644 index 0000000000..5097515ecf --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.coyote.http11.common.HttpStatus; +import org.apache.coyote.http11.common.HttpVersion; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpResponseTest { + + @Test + void 헤더를_추가한다() { + // given + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + httpResponse.addHeader("HEADER", "HI"); + + // then + assertThat(httpResponse.getHeaders().get("HEADER")).isEqualTo("HI"); + } + + @Test + void httpStatus를_설정한다() { + // given + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + httpResponse.setHttpStatus(HttpStatus.OK); + + // then + assertThat(httpResponse.getHttpStatus()).isEqualTo(HttpStatus.OK); + } + + @Test + void 리다이렉트_url을_설정한다() { + // given + final HttpResponse httpResponse = new HttpResponse(HttpVersion.HTTP_1_1); + + // when + httpResponse.sendRedirect("hello.html"); + + // then + assertThat(httpResponse.getRedirect()).isEqualTo("hello.html"); + } +}