diff --git a/README.md b/README.md index f1ace3f84d..e1b385e8b6 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,18 @@ -[x] Cookie에 JSESSIONID 값 저장하기 -[x] Session 구현하기 +## 3단계 - 리팩터링 +-[x] HttpRequest 클래스 구현 +-[x] HttpResponse 클래스 구현 +-[x] Controller 인터페이스 구현 +-[x] AbstractController 추상 클래스 구현 +-[x] RequestMapping 클래스 구현 + ### TODO -[x] 예외 처리 -[x] 매직 넘버 정리 -[x] 리팩터링(클래스 분리, 패키지 정리) --[ ] 상태코드, http 메서드 종류 추가 --[ ] 테스트 - 회원가입, 정적리소스 --[ ] 로깅 +-[x] 상태코드, http 메서드 종류 추가 +-[x] 테스트 - 회원가입, 정적리소스 +-[x] 로깅 +-[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..cfd047f69c 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,18 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(CacheControl.noCache().cachePrivate(), "/**"); + 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..fce9249d04 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() { + ShallowEtagHeaderFilter shallowEtagHeaderFilter = new ShallowEtagHeaderFilter(); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(shallowEtagHeaderFilter); + filterRegistrationBean.addUrlPatterns("/etag", "/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..d42a257f3d 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,9 +2,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.concurrent.TimeUnit; + @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { @@ -20,6 +23,8 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") + .setCacheControl(CacheControl.maxAge(365L, TimeUnit.DAYS).cachePublic()) + .setUseLastModified(true) .addResourceLocations("classpath:/static/"); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..dbd1a8fca7 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -4,6 +4,10 @@ handlebars: server: tomcat: accept-count: 1 - max-connections: 1 + max-connections: 2 threads: max: 2 + + compression: + min-response-size: 10 + enabled: true diff --git a/study/src/test/java/thread/stage0/SynchronizationTest.java b/study/src/test/java/thread/stage0/SynchronizationTest.java index 0333c18e3b..b463c2b984 100644 --- a/study/src/test/java/thread/stage0/SynchronizationTest.java +++ b/study/src/test/java/thread/stage0/SynchronizationTest.java @@ -41,7 +41,7 @@ private static final class SynchronizedMethods { private int sum = 0; - public void calculate() { + public synchronized void calculate() { setSum(getSum() + 1); } diff --git a/study/src/test/java/thread/stage0/ThreadPoolsTest.java b/study/src/test/java/thread/stage0/ThreadPoolsTest.java index 238611ebfe..03efdabc8d 100644 --- a/study/src/test/java/thread/stage0/ThreadPoolsTest.java +++ b/study/src/test/java/thread/stage0/ThreadPoolsTest.java @@ -31,8 +31,8 @@ void testNewFixedThreadPool() { executor.submit(logWithSleep("hello fixed thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; - final int expectedQueueSize = 0; + final int expectedPoolSize = 2; + final int expectedQueueSize = 1; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size()); @@ -46,7 +46,7 @@ void testNewCachedThreadPool() { executor.submit(logWithSleep("hello cached thread pools")); // 올바른 값으로 바꿔서 테스트를 통과시키자. - final int expectedPoolSize = 0; + final int expectedPoolSize = 3; final int expectedQueueSize = 0; assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize()); diff --git a/study/src/test/java/thread/stage1/UserServlet.java b/study/src/test/java/thread/stage1/UserServlet.java index b180a84c32..a78e7b4ad4 100644 --- a/study/src/test/java/thread/stage1/UserServlet.java +++ b/study/src/test/java/thread/stage1/UserServlet.java @@ -11,7 +11,7 @@ public void service(final User user) { join(user); } - private void join(final User user) { + private synchronized void join(final User user) { if (!users.contains(user)) { users.add(user); } diff --git a/tomcat/src/main/java/common/http/AbstractController.java b/tomcat/src/main/java/common/http/AbstractController.java new file mode 100644 index 0000000000..bb14e4a0a7 --- /dev/null +++ b/tomcat/src/main/java/common/http/AbstractController.java @@ -0,0 +1,50 @@ +package common.http; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import static common.http.HttpMethod.DELETE; +import static common.http.HttpMethod.GET; +import static common.http.HttpMethod.PATCH; +import static common.http.HttpMethod.POST; +import static common.http.HttpMethod.PUT; + +public abstract class AbstractController implements Controller { + + public static final String EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD = "요청에 해당하는 메서드가 없습니다."; + private final Map> methodMapping = new EnumMap<>(HttpMethod.class); + + protected AbstractController() { + methodMapping.put(GET, this::doGet); + methodMapping.put(POST, this::doPost); + methodMapping.put(PUT, this::doPut); + methodMapping.put(PATCH, this::doPatch); + methodMapping.put(DELETE, this::doDelete); + } + + @Override + public void service(Request request, Response response) { + methodMapping.get(request.getHttpMethod()).accept(request, response); + } + + protected void doGet(Request request, Response response) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD); + } + + protected void doPost(Request request, Response response) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD); + } + + protected void doPut(Request request, Response response) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD); + } + + protected void doPatch(Request request, Response response) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD); + } + + protected void doDelete(Request request, Response response) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE_WHEN_CALL_NOT_DECLARED_METHOD); + } +} diff --git a/tomcat/src/main/java/common/http/ContentType.java b/tomcat/src/main/java/common/http/ContentType.java new file mode 100644 index 0000000000..caf7787d66 --- /dev/null +++ b/tomcat/src/main/java/common/http/ContentType.java @@ -0,0 +1,42 @@ +package common.http; + +import java.util.Arrays; + +public enum ContentType { + HTML("text/html", "html"), + CSS("text/css", "css"), + JS("text/javascript", "js"), + ICO("image/ico", "ico"), + ; + + public static final String DELIMITER_FOR_EXTENSION = "."; + + private final String type; + private final String extension; + + ContentType(String type, String extension) { + this.type = type; + this.extension = extension; + } + + public static ContentType findByExtension(String extension) { + return Arrays.stream(ContentType.values()) + .filter(contentType -> contentType.extension.equals(extension)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 확장자명 입니다.")); + } + + public static ContentType findByPath(String path) { + int indexBeforeExtension = path.lastIndexOf(DELIMITER_FOR_EXTENSION); + if (indexBeforeExtension == -1) { + throw new IllegalArgumentException("파일의 확장자명이 없습니다."); + } + + String extension = path.substring(indexBeforeExtension + 1); + return findByExtension(extension); + } + + public String getType() { + return type; + } +} diff --git a/tomcat/src/main/java/common/http/Controller.java b/tomcat/src/main/java/common/http/Controller.java new file mode 100644 index 0000000000..30459ae8ae --- /dev/null +++ b/tomcat/src/main/java/common/http/Controller.java @@ -0,0 +1,5 @@ +package common.http; + +public interface Controller { + void service(Request request, Response response); +} diff --git a/tomcat/src/main/java/common/http/ControllerManager.java b/tomcat/src/main/java/common/http/ControllerManager.java new file mode 100644 index 0000000000..ed2e7ff96c --- /dev/null +++ b/tomcat/src/main/java/common/http/ControllerManager.java @@ -0,0 +1,8 @@ +package common.http; + +public interface ControllerManager { + + void add(String path, Controller controller); + + void service(Request request, Response response); +} diff --git a/tomcat/src/main/java/common/http/Cookie.java b/tomcat/src/main/java/common/http/Cookie.java new file mode 100644 index 0000000000..c77922c2c0 --- /dev/null +++ b/tomcat/src/main/java/common/http/Cookie.java @@ -0,0 +1,57 @@ +package common.http; + +import java.util.HashMap; +import java.util.Map; + +public class Cookie { + + private static final String SEPARATOR = "; "; + public static final String DELIMITER = "="; + + private static final int ATTRIBUTE_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map items; + + private Cookie(Map items) { + this.items = items; + } + + public static Cookie from(String cookieHeader) { + if (cookieHeader == null || cookieHeader.isEmpty()) { + return new Cookie(new HashMap<>()); + } + return new Cookie(parse(cookieHeader)); + } + + private static Map parse(String values) { + Map items = new HashMap<>(); + String[] attributesAndValues = values.split(SEPARATOR); + for (String cookie : attributesAndValues) { + String[] attributeAndValue = cookie.split(DELIMITER); + items.put(attributeAndValue[ATTRIBUTE_INDEX], attributeAndValue[VALUE_INDEX].trim()); + } + return items; + } + + public void addAttribute(String attribute, String value) { + items.put(attribute, value); + } + + boolean hasAttribute(String attribute) { + return items.containsKey(attribute); + } + + String getAttribute(String attribute) { + return items.get(attribute); + } + + public String getValue() { + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry item : items.entrySet()) { + stringBuilder.append(item.getKey()).append(DELIMITER).append(item.getValue()).append(SEPARATOR); + } + String cookie = stringBuilder.toString(); + return cookie.substring(0, cookie.length()-2); + } +} diff --git a/tomcat/src/main/java/common/http/Cookies.java b/tomcat/src/main/java/common/http/Cookies.java new file mode 100644 index 0000000000..cc0d0ac7ed --- /dev/null +++ b/tomcat/src/main/java/common/http/Cookies.java @@ -0,0 +1,19 @@ +package common.http; + +public class Cookies { + + private static final String JSESSIONID = "JSESSIONID"; + + private Cookies() {} + + public static Cookie ofJSessionId(String id) { + return Cookie.from(JSESSIONID + Cookie.DELIMITER + id); + } + + public static String getJsessionid(Cookie cookie) { + if (cookie.hasAttribute(JSESSIONID)) { + return cookie.getAttribute(JSESSIONID); + } + throw new IllegalArgumentException("쿠키에 세션 아이디가 없습니다."); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/common/http/HttpMethod.java similarity index 91% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java rename to tomcat/src/main/java/common/http/HttpMethod.java index 1b48458635..a364a3cf69 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java +++ b/tomcat/src/main/java/common/http/HttpMethod.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package common.http; public enum HttpMethod { GET, diff --git a/tomcat/src/main/java/common/http/HttpStatus.java b/tomcat/src/main/java/common/http/HttpStatus.java new file mode 100644 index 0000000000..cfea18cd5d --- /dev/null +++ b/tomcat/src/main/java/common/http/HttpStatus.java @@ -0,0 +1,33 @@ +package common.http; + +public enum HttpStatus { + OK("OK", 200), + CREATED("Created", 201), + ACCEPTED("Accepted", 202), + NO_CONTENT("No Content", 204), + MOVED_PERMANENTLY("Moved Permanently", 301), + FOUND("Found", 302), + PERMANENT_REDIRECT("Permanent Redirect", 308), + BAD_REQUEST("Bad Request", 400), + UNAUTHORIZED("Unauthorized", 401), + FORBIDDEN("Forbidden", 403), + NOT_FOUND("Not Found", 404), + CONFLICT("Conflict", 409), + INTERNAL_SERVER_ERROR("Internal Server Error", 500); + + private final String statusMessage; + private final int statusCode; + + HttpStatus(String statusMessage, int statusCode) { + this.statusMessage = statusMessage; + this.statusCode = statusCode; + } + + public String getStatusMessage() { + return statusMessage; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/tomcat/src/main/java/common/http/Request.java b/tomcat/src/main/java/common/http/Request.java new file mode 100644 index 0000000000..e4d7dca5a0 --- /dev/null +++ b/tomcat/src/main/java/common/http/Request.java @@ -0,0 +1,28 @@ +package common.http; + +public interface Request { + + HttpMethod getHttpMethod(); + + String getVersionOfTheProtocol(); + + String getAccount(); + + String getPassword(); + + Session getSession(boolean create); + + Session getSession(); + + String getCookie(); + + String getPath(); + + boolean hasValidSession(); + + String getEmail(); + + void addSession(Session session); + + boolean hasStaticResourcePath(); +} diff --git a/tomcat/src/main/java/common/http/Response.java b/tomcat/src/main/java/common/http/Response.java new file mode 100644 index 0000000000..65b31dcaf0 --- /dev/null +++ b/tomcat/src/main/java/common/http/Response.java @@ -0,0 +1,30 @@ +package common.http; + +public interface Response { + + void addVersionOfTheProtocol(String versionOfTheProtocol); + + void addHttpStatus(HttpStatus httpStatus); + + void addContentType(ContentType contentType); + + void sendRedirect(String redirectURL); + + void addStaticResourcePath(String name); + + void addCookie(Cookie cookie); + + boolean hasStaticResourcePath(); + + String getStaticResourcePath(); + + void addBody(String body); + + String getMessage(); + + void addException(Exception e); + + String getException(); + + boolean hasException(); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java b/tomcat/src/main/java/common/http/Session.java similarity index 93% rename from tomcat/src/main/java/org/apache/coyote/http11/session/Session.java rename to tomcat/src/main/java/common/http/Session.java index 25197dba4b..a5deecd9c1 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java +++ b/tomcat/src/main/java/common/http/Session.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.session; +package common.http; import java.util.HashMap; import java.util.Map; diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index 3dd7593507..6b2ab04917 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,11 +1,19 @@ package nextstep; +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(); + + tomcat.addController("/", new HomeController()); + tomcat.addController("/login", new LoginController()); + tomcat.addController("/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..85929c9e21 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,19 @@ +package nextstep.jwp.controller; + +import common.http.AbstractController; +import common.http.Request; +import common.http.Response; + +import static common.http.ContentType.HTML; +import static common.http.HttpStatus.OK; + +public class HomeController extends AbstractController { + + @Override + protected void doGet(Request request, Response response) { + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(OK); + response.addContentType(HTML); + response.addBody("Hello world!"); + } +} 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..f9284c99e6 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,61 @@ +package nextstep.jwp.controller; + +import common.http.AbstractController; +import common.http.Cookies; +import common.http.HttpStatus; +import common.http.Request; +import common.http.Response; +import common.http.Session; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static common.http.HttpStatus.FOUND; +import static common.http.HttpStatus.OK; +import static common.http.HttpStatus.UNAUTHORIZED; + +public class LoginController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + + @Override + protected void doGet(Request request, Response response) { + if (!request.hasValidSession()) { + buildStaticResourceResponse(request, response, OK, "/login.html"); + return; + } + + Session session = request.getSession(); + buildRedirectResponse(request, response, session); + } + + @Override + protected void doPost(Request request, Response response) { + User user = InMemoryUserRepository.findByAccount(request.getAccount()) + .orElseThrow(() -> new IllegalArgumentException("회원 정보가 존재하지 않습니다.")); + + if (!user.checkPassword(request.getPassword())) { + buildStaticResourceResponse(request, response, UNAUTHORIZED, "/401.html"); + return; + } + + log.info("user: {}", user); + Session session = request.getSession(true); + session.setAttribute("user", user); + buildRedirectResponse(request, response, session); + } + + private void buildStaticResourceResponse(Request request, Response response, HttpStatus httpStatus, String path) { + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(httpStatus); + response.addStaticResourcePath(path); + } + + private void buildRedirectResponse(Request request, Response response, Session session) { + response.addCookie(Cookies.ofJSessionId(session.getId())); + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(FOUND); + response.sendRedirect("/index.html"); + } +} 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..30b34d4329 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,40 @@ +package nextstep.jwp.controller; + +import common.http.AbstractController; +import common.http.Cookies; +import common.http.Request; +import common.http.Response; +import common.http.Session; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; + +import static common.http.HttpStatus.FOUND; +import static common.http.HttpStatus.OK; + +public class RegisterController extends AbstractController { + + @Override + protected void doGet(Request request, Response response) { + // 로그아웃이 없으므로 모든 요청에 대해 진행합니다. + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(OK); + response.addStaticResourcePath("/register.html"); + } + + @Override + protected void doPost(Request request, Response response) { + if (InMemoryUserRepository.findByAccount(request.getAccount()).isPresent()) { + throw new IllegalArgumentException("이미 가입된 회원입니다."); + } + + User user = new User(request.getAccount(), request.getPassword(), request.getEmail()); + InMemoryUserRepository.save(user); + + Session session = request.getSession(true); + session.setAttribute("user", user); + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(FOUND); + response.addCookie(Cookies.ofJSessionId(session.getId())); + response.sendRedirect("/index.html"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 9e5d38a1d9..ebfb8f0d8c 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -25,7 +25,7 @@ public static void save(User user) { if (user.equals(database.get(user.getAccount()))) { throw new IllegalArgumentException("이미 가입된 유저입니다."); } - throw new IllegalArgumentException("잘못된 유저 정보입니다."); + throw new SecurityException("잘못된 유저 정보입니다."); } user.setId(id.getAndIncrement()); diff --git a/tomcat/src/main/java/org/apache/Constants.java b/tomcat/src/main/java/org/apache/Constants.java new file mode 100644 index 0000000000..bb9187bde2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/Constants.java @@ -0,0 +1,9 @@ +package org.apache; + +public class Constants { + + public static final String SPACE = " "; + public static final String CRLF = "\r\n"; + + private Constants() {} +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java similarity index 96% rename from tomcat/src/main/java/org/apache/coyote/http11/session/Manager.java rename to tomcat/src/main/java/org/apache/catalina/Manager.java index 0929ea39c9..be621e9da5 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,4 +1,6 @@ -package org.apache.coyote.http11.session; +package org.apache.catalina; + +import common.http.Session; import java.io.IOException; diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..5f9ad66f12 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,5 +1,6 @@ package org.apache.catalina.connector; +import common.http.ControllerManager; import org.apache.coyote.http11.Http11Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,6 +9,8 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class Connector implements Runnable { @@ -15,17 +18,23 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final int DEFAULT_THREAD_COUNT = 250; private final ServerSocket serverSocket; + private final ControllerManager controllerManager; + private final ExecutorService executorService; + private boolean stopped; - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + public Connector(ControllerManager controllerManager) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, controllerManager, DEFAULT_THREAD_COUNT); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, ControllerManager controllerManager, final int maxThreads) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.controllerManager = controllerManager; + this.executorService = Executors.newFixedThreadPool(maxThreads); } private ServerSocket createServerSocket(final int port, final int acceptCount) { @@ -39,7 +48,7 @@ private ServerSocket createServerSocket(final int port, final int acceptCount) { } public void start() { - var thread = new Thread(this); + Thread thread = new Thread(this); thread.setDaemon(true); thread.start(); stopped = false; @@ -66,8 +75,8 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); - new Thread(processor).start(); + Runnable processor = new Http11Processor(connection, controllerManager); + executorService.execute(processor); } public void stop() { @@ -80,8 +89,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/startup/DynamicControllerManager.java b/tomcat/src/main/java/org/apache/catalina/startup/DynamicControllerManager.java new file mode 100644 index 0000000000..a50a4a23da --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/startup/DynamicControllerManager.java @@ -0,0 +1,58 @@ +package org.apache.catalina.startup; + +import common.http.Controller; +import common.http.ControllerManager; +import common.http.Cookie; +import common.http.Cookies; +import common.http.Request; +import common.http.Response; +import common.http.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class DynamicControllerManager implements ControllerManager { + + private static final Map mapper = new HashMap<>(); + private static final SessionManager sessionManager = new SessionManager(); + private static final Logger log = LoggerFactory.getLogger(DynamicControllerManager.class); + + @Override + public void add(String path, Controller controller) { + mapper.put(path, controller); + } + + @Override + public void service(Request request, Response response) { + findSessionByCookie(request); + + Controller controller = mapper.get(request.getPath()); + if (controller == null) { + return; + } + + log.info("Controller: {}", controller.getClass().getName()); + controller.service(request, response); + + saveNewSession(request); + } + + private void findSessionByCookie(Request request) { + String cookieHeader = request.getCookie(); + if (cookieHeader != null) { + Cookie cookie = Cookie.from(cookieHeader); + String jsessionid = Cookies.getJsessionid(cookie); + Session session = sessionManager.findSession(jsessionid); + request.addSession(session); + } + } + + private void saveNewSession(Request request) { + Session session = request.getSession(); + if (session != null && !sessionManager.hasSession(session)) { + sessionManager.add(session); + } + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/startup/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/startup/SessionManager.java new file mode 100644 index 0000000000..66ff5282d9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/startup/SessionManager.java @@ -0,0 +1,55 @@ +package org.apache.catalina.startup; + +import ch.qos.logback.core.spi.LifeCycle; +import common.http.Session; +import org.apache.catalina.Manager; + +public class SessionManager implements Manager, LifeCycle { + + private static final Sessions sessions = new Sessions(); + + private boolean isStarted = false; + + public SessionManager() { + start(); + } + + @Override + public void add(Session session) { + sessions.add(session.getId(), session); + } + + @Override + public Session findSession(String id) { + return sessions.get(id); + } + + @Override + public void remove(Session session) { + sessions.remove(session.getId()); + } + + @Override + public void start() { + if (!isStarted) { + isStarted = true; + } + } + + @Override + public void stop() { + if (isStarted) { + sessions.clear(); + isStarted = false; + } + } + + @Override + public boolean isStarted() { + return isStarted; + } + + public boolean hasSession(Session session) { + return sessions.get(session); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Sessions.java b/tomcat/src/main/java/org/apache/catalina/startup/Sessions.java new file mode 100644 index 0000000000..45a611e38e --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/startup/Sessions.java @@ -0,0 +1,34 @@ +package org.apache.catalina.startup; + +import common.http.Session; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class Sessions { + + private static final Map idAndSessions = new ConcurrentHashMap<>(); + + void add(String id, Session session) { + idAndSessions.put(id, session); + } + + Session get(String id) { + if (!idAndSessions.containsKey(id)) { + return null; + } + return idAndSessions.get(id); + } + + void remove(String id) { + idAndSessions.remove(id); + } + + void clear() { + idAndSessions.clear(); + } + + boolean get(Session session) { + return idAndSessions.containsValue(session); + } +} 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..88a9356d67 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,5 +1,7 @@ package org.apache.catalina.startup; +import common.http.Controller; +import common.http.ControllerManager; import org.apache.catalina.connector.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,8 +12,18 @@ public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + private final ControllerManager controllerManager; + + public Tomcat() { + this.controllerManager = new DynamicControllerManager(); + } + + public void addController(String path, Controller controller) { + controllerManager.add(path, controller); + } + public void start() { - var connector = new Connector(); + Connector connector = new Connector(controllerManager); connector.start(); try { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java deleted file mode 100644 index 4e97afeed3..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.apache.coyote.http11; - -public enum ContentType { - HTML("text/html"), - CSS("text/css"), - JS("text/javascript"), - ICO("image/ico"); - - private final String type; - - ContentType(String type) { - this.type = type; - } - - public String getType() { - return type; - } -} 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 1d2f83835b..ec755a1a85 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,8 +1,13 @@ package org.apache.coyote.http11; -import nextstep.jwp.exception.UncheckedServletException; +import common.http.ControllerManager; +import common.http.Request; +import common.http.Response; import org.apache.coyote.Processor; -import org.apache.coyote.http11.session.SessionManager; +import org.apache.coyote.http11.controller.ExceptionController; +import org.apache.coyote.http11.controller.StaticControllerManager; +import org.apache.coyote.http11.request.HttpRequestParser; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,12 +19,14 @@ public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - private static final SessionManager sessionManager = new SessionManager(); private final Socket connection; - public Http11Processor(Socket connection) { + private ControllerManager controllerManager; + + public Http11Processor(Socket connection, ControllerManager controllerManager) { this.connection = connection; + this.controllerManager = controllerManager; } @Override @@ -34,14 +41,37 @@ public void process(final Socket connection) { final var outputStream = connection.getOutputStream(); final var reader = new BufferedReader(new InputStreamReader(inputStream)) ) { - HttpRequestParser requestParser = HttpRequestParser.from(reader); + Request httpRequest = HttpRequestParser.parse(reader); + Response httpResponse = new HttpResponse(); + + log.info("Path: {}, Method: {}", httpRequest.getPath(), httpRequest.getHttpMethod()); - String response = HttpResponseMaker.makeFrom(requestParser, sessionManager); + processService(httpRequest, httpResponse); + serviceIfHasStaticResourcePath(httpRequest, httpResponse); - outputStream.write(response.getBytes()); + log.info(httpResponse.getMessage()); + outputStream.write(httpResponse.getMessage().getBytes()); outputStream.flush(); - } catch (IOException | UncheckedServletException | IllegalArgumentException e) { + } catch (IOException e) { log.error(e.getMessage(), e); } } + + private void processService(Request httpRequest, Response httpResponse) { + try { + controllerManager.service(httpRequest, httpResponse); + } catch (Exception e) { + log.error(e.getMessage(), e); + httpResponse.addException(e); + ExceptionController exceptionController = new ExceptionController(); + exceptionController.service(httpRequest, httpResponse); + } + } + + private void serviceIfHasStaticResourcePath(Request httpRequest, Response httpResponse) { + if (httpRequest.hasStaticResourcePath() || httpResponse.hasException() || httpResponse.hasStaticResourcePath()) { + controllerManager = StaticControllerManager.getInstance(); + controllerManager.service(httpRequest, httpResponse); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestFirstLineInfo.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestFirstLineInfo.java deleted file mode 100644 index ac4f1fbac2..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestFirstLineInfo.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.apache.coyote.http11; - -public class HttpRequestFirstLineInfo { - - private static final String SPACE = " "; - private static final String QUERY_STRING_DELIMITER = "?"; - - private static final int HTTP_METHOD_INDEX = 0; - private static final int URI_INDEX = 1; - private static final int PROTOCOL_VERSION_INDEX = 2; - private static final int NUMBER_OF_FIRST_LINE_INFOS = 3; - - private final HttpMethod httpMethod; - private final String uri; - private final String versionOfTheProtocol; - - private HttpRequestFirstLineInfo(HttpMethod httpMethod, String uri, String versionOfTheProtocol) { - this.httpMethod = httpMethod; - this.uri = uri; - this.versionOfTheProtocol = versionOfTheProtocol; - } - - public static HttpRequestFirstLineInfo from(String firstLine) { - String[] infos = firstLine.split(SPACE); - if (infos.length != NUMBER_OF_FIRST_LINE_INFOS) { - throw new IllegalArgumentException("유효하지 않은 Request-line 입니다."); - } - - HttpMethod httpMethod = HttpMethod.parseHttpMethod(infos[HTTP_METHOD_INDEX]); - String uriWithQueryString = infos[URI_INDEX]; - String versionOfTheProtocol = infos[PROTOCOL_VERSION_INDEX]; - - int indexOfQueryStringDelimiter = uriWithQueryString.indexOf(QUERY_STRING_DELIMITER); - if (indexOfQueryStringDelimiter != -1) { - String uri = uriWithQueryString.substring(0, indexOfQueryStringDelimiter); - return new HttpRequestFirstLineInfo(httpMethod, uri, versionOfTheProtocol); - } - - return new HttpRequestFirstLineInfo(httpMethod, uriWithQueryString, versionOfTheProtocol); - } - - public HttpMethod getHttpMethod() { - return httpMethod; - } - - public String getUri() { - return uri; - } - - public String getVersionOfTheProtocol() { - return versionOfTheProtocol; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java deleted file mode 100644 index f5923595ca..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.BufferedReader; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -public class HttpRequestParser { - - private static final String DELIMITER = ":"; - private static final String PARAMS_DELIMITER = "&"; - private static final String PARAM_VALUE_DELIMITER = "="; - - private static final int PROPERTY_INDEX = 0; - private static final int VALUE_INDEX = 1; - private static final int START_AFTER_SPACE = 2; - - private final HttpRequestFirstLineInfo httpRequestFirstLineInfo; - private final Map headers; - private final Map body; - - - public HttpRequestParser( - HttpRequestFirstLineInfo httpRequestFirstLineInfo, - Map headers, - Map body - ) { - this.httpRequestFirstLineInfo = httpRequestFirstLineInfo; - this.headers = headers; - this.body = body; - } - - public static HttpRequestParser from(BufferedReader reader) throws IOException { - HttpRequestFirstLineInfo firstLineInfo = parseFirstLine(reader); - Map headers = parseHeaders(reader); - Map body = parseBody(reader, headers); - - return new HttpRequestParser(firstLineInfo, headers, body); - } - - private static HttpRequestFirstLineInfo parseFirstLine(BufferedReader reader) throws IOException { - String requestLine = reader.readLine(); - if (requestLine == null) { - throw new IOException("요청에 Reqeust-line이 없습니다."); - } - return HttpRequestFirstLineInfo.from(requestLine); - } - - private static Map parseHeaders(BufferedReader reader) throws IOException { - Map headers = new HashMap<>(); - - String headerLine; - while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) { - int indexOfDelimiter = headerLine.indexOf(DELIMITER); - if (indexOfDelimiter != -1) { - String property = headerLine.substring(0, indexOfDelimiter); - String value = headerLine.substring(indexOfDelimiter + START_AFTER_SPACE); - headers.put(property, value); - } - } - - return headers; - } - - private static Map parseBody(BufferedReader reader, Map headers) throws IOException { - int contentLength = Integer.parseInt(headers.getOrDefault("Content-Length", "0")); - - if (contentLength > 0) { - char[] buffer = new char[contentLength]; - reader.read(buffer, 0, contentLength); - String requestBody = new String(buffer); - - Map body = Arrays.stream(requestBody.split(PARAMS_DELIMITER)) - .map(property -> property.split(PARAM_VALUE_DELIMITER)) - .filter(propertyAndValue -> propertyAndValue.length == 2) - .collect(Collectors.toMap(data -> data[PROPERTY_INDEX], data -> data[VALUE_INDEX])); - - if (body.size() != requestBody.split(PARAMS_DELIMITER).length) { - throw new IllegalArgumentException("요청의 바디가 잘못되었습니다."); - } - - return body; - } - - return new HashMap<>(); - } - - public HttpRequestFirstLineInfo getHttpRequestFirstLineInfo() { - return httpRequestFirstLineInfo; - } - - public Map getHeaders() { - return headers; - } - - public Map getBody() { - return body; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseMaker.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseMaker.java deleted file mode 100644 index e499dbd997..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponseMaker.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.apache.coyote.http11; - -import nextstep.jwp.db.InMemoryUserRepository; -import nextstep.jwp.model.User; -import org.apache.coyote.http11.session.HttpCookie; -import org.apache.coyote.http11.session.Session; -import org.apache.coyote.http11.session.SessionManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -import static org.apache.coyote.http11.RequestMapper.*; - -public class HttpResponseMaker { - - public static final String COOKIE = "Cookie"; - private static final String CRLF = "\r\n"; - private static final String SPACE = " "; - private static final String ACCOUNT = "account"; - private static final String PASSWORD = "password"; - private static final String STATIC_RESOURCE_DIR = "static"; - private static final String EMAIL = "email"; - private static final Logger log = LoggerFactory.getLogger(HttpResponseMaker.class); - - public static String makeFrom(HttpRequestParser httpRequestParser, SessionManager sessionManager) throws IOException { - HttpRequestFirstLineInfo firstLineInfo = httpRequestParser.getHttpRequestFirstLineInfo(); - RequestMapper mapper = RequestMapper.findMapper(firstLineInfo); - Map headers = httpRequestParser.getHeaders(); - Map body = httpRequestParser.getBody(); - - if (mapper == LOG_IN) { - HttpCookie cookie = HttpCookie.from(headers.get(COOKIE)); - - if (hasValidCookie(sessionManager, cookie)) { - return buildRedirectResponse(firstLineInfo, RedirectLocation.LOG_IN_SUCCESS, mapper); - } - - return buildResponse(firstLineInfo, mapper, readStaticResource(mapper)); - } - - if (mapper == LOG_IN_WITH_INFOS) { - return handleLoginRequest(firstLineInfo, headers, body, mapper, sessionManager); - } - - if (mapper == REGISTER_WITH_INFOS) { - return handleRegisterRequest(firstLineInfo, body, mapper); - } - - String responseBody = "Hello world!"; - - if (!mapper.getPath().equals("/")) { - responseBody = readStaticResource(mapper); - } - - return buildResponse(firstLineInfo, mapper, responseBody); - } - - private static boolean hasValidCookie(SessionManager sessionManager, HttpCookie cookie) { - return cookie.hasJSessionId() && sessionManager.findSession(cookie.getJSessionId()) != null; - } - - private static String handleLoginRequest( - HttpRequestFirstLineInfo firstLineInfo, - Map headers, - Map body, - RequestMapper mapper, - SessionManager sessionManager - ) { - User user = InMemoryUserRepository.findByAccount(body.get(ACCOUNT)) - .orElseThrow(() -> new IllegalArgumentException("회원 정보가 존재하지 않습니다.")); - - if (!user.checkPassword(body.get(PASSWORD))) { - return buildRedirectResponse(firstLineInfo, RedirectLocation.LOG_IN_FAIL, mapper); - } - log.info("user: {}", user); - - String cookieHeader = headers.get(COOKIE); - HttpCookie cookie = HttpCookie.from(cookieHeader); - cookie.bake(); - Session session = new Session(cookie.getJSessionId()); - session.setAttribute("user", user); - sessionManager.add(session); - - return String.join(CRLF, - buildFirstLine(firstLineInfo, mapper), - "Location:" + SPACE + RedirectLocation.LOG_IN_SUCCESS.getRedirectUrl(), - "Set-Cookie:" + SPACE + "JSESSIONID=" + session.getId()); - } - - private static String handleRegisterRequest( - HttpRequestFirstLineInfo firstLineInfo, - Map body, - RequestMapper mapper - ) { - if (InMemoryUserRepository.findByAccount(body.get(ACCOUNT)).isPresent()) { - throw new IllegalArgumentException("이미 가입된 회원입니다."); - } - - User user = new User(body.get(ACCOUNT), body.get(PASSWORD), body.get(EMAIL)); - InMemoryUserRepository.save(user); - - return buildRedirectResponse(firstLineInfo, RedirectLocation.REGISTER, mapper); - } - - private static String readStaticResource(RequestMapper mapper) throws IOException { - ClassLoader classLoader = HttpResponseMaker.class.getClassLoader(); - URL resource = classLoader.getResource(STATIC_RESOURCE_DIR + mapper.getPath()); - - if (resource == null) { - throw new FileNotFoundException("해당하는 파일을 찾을 수 없습니다."); - } - - Path path = new File(resource.getFile()).toPath(); - - return new String(Files.readAllBytes(path)); - } - - private static String buildRedirectResponse( - HttpRequestFirstLineInfo firstLineInfo, - RedirectLocation redirectLocation, - RequestMapper mapper - ) { - return String.join(CRLF, - buildFirstLine(firstLineInfo, mapper), - "Location:" + SPACE + redirectLocation.getRedirectUrl()); - } - - private static String buildResponse(HttpRequestFirstLineInfo firstLineInfo, RequestMapper mapper, String responseBody) { - return String.join(CRLF, - buildFirstLine(firstLineInfo, mapper), - "Content-Type:" + SPACE + mapper.getContentType().getType() + ";charset=utf-8" + SPACE, - "Content-Length:" + SPACE + responseBody.getBytes().length + SPACE, - "", - responseBody); - } - - private static String buildFirstLine(HttpRequestFirstLineInfo firstLineInfo, RequestMapper mapper) { - return String.join(CRLF, firstLineInfo.getVersionOfTheProtocol() + SPACE + mapper.getHttpStatus().getStatusCode() + SPACE + mapper.getHttpStatus().getStatus() + SPACE); - } - - private enum RedirectLocation { - LOG_IN_SUCCESS("/index.html"), - LOG_IN_FAIL("/401.html"), - REGISTER("/index.html"); - - private final String redirectUrl; - - RedirectLocation(String redirectUrl) { - this.redirectUrl = redirectUrl; - } - - public String getRedirectUrl() { - return redirectUrl; - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java deleted file mode 100644 index 8be9ed417b..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.apache.coyote.http11; - -public enum HttpStatus { - OK("OK", 200), - CREATED("Created", 201), - FOUND("Found", 302), - UNAUTHORIZED("Unauthorized", 401), - NOT_FOUND("Not Found", 404), - INTERNAL_SERVER_ERROR("Internal Server Error", 500); - - private final String status; - private final int statusCode; - - HttpStatus(String status, int statusCode) { - this.status = status; - this.statusCode = statusCode; - } - - public String getStatus() { - return status; - } - - public int getStatusCode() { - return statusCode; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestMapper.java deleted file mode 100644 index 30579bfa26..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.Arrays; - -import static org.apache.coyote.http11.ContentType.*; -import static org.apache.coyote.http11.HttpMethod.GET; -import static org.apache.coyote.http11.HttpMethod.POST; -import static org.apache.coyote.http11.HttpStatus.FOUND; -import static org.apache.coyote.http11.HttpStatus.OK; - -public enum RequestMapper { - HOME("/", "/", GET, OK, HTML), - INDEX("/index.html", "/index.html", GET, OK, HTML), - SCRIPTS_JS("/js/scripts.js", "/js/scripts.js", GET, OK, JS), - CHART_AREA_JS("/assets/chart-area.js", "/assets/chart-area.js", GET, OK, JS), - CHART_BAR_JS("/assets/chart-bar.js", "/assets/chart-bar.js", GET, OK, JS), - CHART_PIE_JS("/assets/chart-pie.js", "/assets/chart-pie.js", GET, OK, JS), - FAVICON("/favicon.ico", "/favicon.ico", GET, OK, ICO), - CSS("/css/styles.css", "/css/styles.css", GET, OK, ContentType.CSS), - LOG_IN("/login", "/login.html", GET, FOUND, HTML), - LOG_IN_WITH_INFOS("/login", null, POST, FOUND, HTML), - UNAUTHORIZED("/401.html", "/401.html", GET, OK, HTML), - REGISTER("/register", "/register.html", GET, OK, HTML), - REGISTER_WITH_INFOS("/register", null, POST, FOUND, HTML), - ; - - private final String uri; - private final String path; - private final HttpMethod httpMethod; - private final HttpStatus httpStatus; - private final ContentType contentType; - - RequestMapper(String uri, String path, HttpMethod httpMethod, HttpStatus httpStatus, ContentType contentType) { - this.uri = uri; - this.path = path; - this.httpMethod = httpMethod; - this.httpStatus = httpStatus; - this.contentType = contentType; - } - - public static RequestMapper findMapper(HttpRequestFirstLineInfo httpRequestFirstLineInfo) { - return Arrays.stream(RequestMapper.values()) - .filter(requestMapper -> httpRequestFirstLineInfo.getHttpMethod().equals(requestMapper.httpMethod)) - .filter(requestMapper -> httpRequestFirstLineInfo.getUri().equals(requestMapper.uri)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("잘못된 요청입니다.")); - } - - public String getPath() { - return path; - } - - public HttpStatus getHttpStatus() { - return httpStatus; - } - - public ContentType getContentType() { - return contentType; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/ExceptionController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/ExceptionController.java new file mode 100644 index 0000000000..0c27720de1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/ExceptionController.java @@ -0,0 +1,56 @@ +package org.apache.coyote.http11.controller; + +import common.http.Controller; +import common.http.HttpStatus; +import common.http.Request; +import common.http.Response; + +import java.util.Arrays; + +public class ExceptionController implements Controller { + + @Override + public void service(Request request, Response response) { + String exception = response.getException(); + HttpStatus status = ExceptionType.getStatus(exception); + String pageName = ExceptionType.getExceptionPage(exception); + response.addHttpStatus(status); + response.addStaticResourcePath(pageName); + } + + private enum ExceptionType { + UNAUTHORIZED(SecurityException.class, HttpStatus.UNAUTHORIZED, "/401.html"), + BAD_REQUEST(IllegalArgumentException.class, HttpStatus.BAD_REQUEST, "/404.html"), + CONFLICT(IllegalStateException.class, HttpStatus.CONFLICT, "/409.html"), + INTERNAL_SERVER_ERROR(Exception.class, HttpStatus.INTERNAL_SERVER_ERROR, "/500.html") + ; + + private final Class exceptionClass; + private final HttpStatus httpStatus; + private final String filePath; + + ExceptionType(Class exceptionClass, HttpStatus httpStatus, String filePath) { + this.exceptionClass = exceptionClass; + this.httpStatus = httpStatus; + this.filePath = filePath; + } + + public static HttpStatus getStatus(String exception) { + ExceptionType type = Arrays.stream(ExceptionType.values()) + .filter(exceptionType -> exceptionType.exceptionClass.getName().equals(exception)) + .findFirst() + .orElse(INTERNAL_SERVER_ERROR); + + return type.httpStatus; + } + + public static String getExceptionPage(String exception) { + ExceptionType type = Arrays.stream(ExceptionType.values()) + .filter(exceptionType -> exceptionType.exceptionClass.getName().equals(exception)) + .findFirst() + .orElse(INTERNAL_SERVER_ERROR); + + return type.filePath; + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticControllerManager.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticControllerManager.java new file mode 100644 index 0000000000..cf2a0679e9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticControllerManager.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.controller; + +import common.http.Controller; +import common.http.ControllerManager; +import common.http.Request; +import common.http.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StaticControllerManager implements ControllerManager { + + private static final StaticControllerManager instance = new StaticControllerManager(); + private static final Controller controller = new StaticResourceController(); + private static final Logger log = LoggerFactory.getLogger(StaticControllerManager.class); + + private StaticControllerManager() {} + + public static StaticControllerManager getInstance() { + return instance; + } + + @Override + public void add(String path, Controller controller) { + throw new IllegalStateException("컨트롤러를 추가할 수 없습니다."); + } + + @Override + public void service(Request request, Response response) { + log.info("Controller: {}", this.getClass().getName()); + controller.service(request, response); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticResourceController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticResourceController.java new file mode 100644 index 0000000000..c7162bdc8d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticResourceController.java @@ -0,0 +1,70 @@ +package org.apache.coyote.http11.controller; + +import common.http.ContentType; +import common.http.Controller; +import common.http.Request; +import common.http.Response; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static common.http.HttpStatus.OK; + +public class StaticResourceController implements Controller { + + private static final String STATIC_RESOURCE_DIR = "static"; + + @Override + public void service(Request request, Response response) { + if (response.hasException()) { + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + buildResponse(response, response.getStaticResourcePath()); + return; + } + + if (response.hasStaticResourcePath()) { + buildResponse(response, response.getStaticResourcePath()); + return; + } + + response.addVersionOfTheProtocol(request.getVersionOfTheProtocol()); + response.addHttpStatus(OK); + buildResponse(response, request.getPath()); + } + + private void buildResponse(Response response, String path) { + try { + URL resource = getResource(path); + String fileContent = readFileContent(resource); + ContentType contentType = ContentType.findByPath(path); + + response.addContentType(contentType); + + if (fileContent.length() != 0) { + response.addBody(fileContent); + } + } catch (FileNotFoundException e) { + throw new IllegalArgumentException("해당하는 파일을 찾을 수 없습니다."); + } catch (IOException e) { + throw new IllegalArgumentException("파일을 읽을 수 없습니다."); + } + } + + private URL getResource(String path) throws FileNotFoundException { + ClassLoader classLoader = StaticResourceController.class.getClassLoader(); + URL resource = classLoader.getResource(STATIC_RESOURCE_DIR + path); + + if (resource == null) { + throw new FileNotFoundException("해당하는 파일을 찾을 수 없습니다."); + } + + return resource; + } + + private String readFileContent(URL resource) throws IOException { + return new String(Files.readAllBytes(Paths.get(resource.getFile()))); + } +} 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..313fdbf315 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,86 @@ +package org.apache.coyote.http11.request; + +import common.http.HttpMethod; +import common.http.Request; +import common.http.Session; + +import java.util.UUID; +import java.util.regex.Pattern; + +public class HttpRequest implements Request { + + private static final Pattern STATIC_RESOURCE_PATH_PATTERN = Pattern.compile("\\.[a-zA-Z0-9]+$"); + + private final HttpRequestLine httpRequestLine; + private final HttpRequestHeaders httpRequestHeaders; + private final HttpRequestBody httpRequestBody; + private Session session; + + public HttpRequest(HttpRequestLine httpRequestLine, HttpRequestHeaders httpRequestHeaders, HttpRequestBody httpRequestBody) { + this.httpRequestLine = httpRequestLine; + this.httpRequestHeaders = httpRequestHeaders; + this.httpRequestBody = httpRequestBody; + } + + @Override + public HttpMethod getHttpMethod() { + return httpRequestLine.getHttpMethod(); + } + + @Override + public String getPath() { + return httpRequestLine.getPath(); + } + + @Override + public String getVersionOfTheProtocol() { + return httpRequestLine.getVersionOfTheProtocol(); + } + + @Override + public String getCookie() { + return httpRequestHeaders.getCookie(); + } + + @Override + public String getAccount() { + return httpRequestBody.getAccount(); + } + + @Override + public String getPassword() { + return httpRequestBody.getPassword(); + } + + @Override + public String getEmail() { + return httpRequestBody.getEmail(); + } + + @Override + public Session getSession() { + return session; + } + + @Override + public Session getSession(boolean create) { + UUID uuid = UUID.randomUUID(); + this.session = new Session(uuid.toString()); + return session; + } + + @Override + public boolean hasValidSession() { + return session != null; + } + + @Override + public boolean hasStaticResourcePath() { + return STATIC_RESOURCE_PATH_PATTERN.matcher(getPath()).find(); + } + + @Override + public void addSession(Session session) { + this.session = session; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java new file mode 100644 index 0000000000..ff028698bf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; + +public class HttpRequestBody { + + private final Map body; + + HttpRequestBody(Map body) { + this.body = body; + } + + String getAccount() { + return body.get("account"); + } + + String getPassword() { + return body.get("password"); + } + + String getEmail() { + return body.get("email"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java new file mode 100644 index 0000000000..38a1b4ec60 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java @@ -0,0 +1,20 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; + +public class HttpRequestHeaders { + + private final Map headers; + + HttpRequestHeaders(Map headers) { + this.headers = headers; + } + + int getContentLength() { + return Integer.parseInt(headers.getOrDefault("Content-Length", "0")); + } + + String getCookie() { + return headers.get("Cookie"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java new file mode 100644 index 0000000000..18bbdeacfc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java @@ -0,0 +1,62 @@ +package org.apache.coyote.http11.request; + +import common.http.HttpMethod; + +public class HttpRequestLine { + + private static final String SPACE = " "; + private static final String QUERY_STRING_DELIMITER = "?"; + + private static final int HTTP_METHOD_INDEX = 0; + private static final int PATH_INDEX = 1; + private static final int PROTOCOL_VERSION_INDEX = 2; + private static final int NUMBER_OF_FIRST_LINE_INFOS = 3; + + private final HttpMethod httpMethod; + private final String path; + private final String versionOfTheProtocol; + + private HttpRequestLine(HttpMethod httpMethod, String path, String versionOfTheProtocol) { + this.httpMethod = httpMethod; + this.path = path; + this.versionOfTheProtocol = versionOfTheProtocol; + } + + static HttpRequestLine from(String requestLine) { + String[] infos = parseInfos(requestLine); + + HttpMethod httpMethod = HttpMethod.parseHttpMethod(infos[HTTP_METHOD_INDEX]); + String uri = removeQueryString(infos[PATH_INDEX]); + String versionOfTheProtocol = infos[PROTOCOL_VERSION_INDEX]; + + return new HttpRequestLine(httpMethod, uri, versionOfTheProtocol); + } + + private static String removeQueryString(String uri) { + int indexOfQueryStringDelimiter = uri.indexOf(QUERY_STRING_DELIMITER); + if (indexOfQueryStringDelimiter != -1) { + return uri.substring(0, indexOfQueryStringDelimiter); + } + return uri; + } + + private static String[] parseInfos(String requestLine) { + String[] infos = requestLine.split(SPACE); + if (infos.length != NUMBER_OF_FIRST_LINE_INFOS) { + throw new IllegalArgumentException("유효하지 않은 Request-line 입니다."); + } + return infos; + } + + HttpMethod getHttpMethod() { + return httpMethod; + } + + String getPath() { + return path; + } + + String getVersionOfTheProtocol() { + return versionOfTheProtocol; + } +} 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..5b25132e22 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java @@ -0,0 +1,87 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequestParser { + + private static final String DELIMITER = ":"; + private static final String PARAMS_DELIMITER = "&"; + private static final String PARAM_VALUE_DELIMITER = "="; + + private static final int PROPERTY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final int START_AFTER_SPACE = 2; + + public static HttpRequest parse(BufferedReader reader) throws IOException { + HttpRequestLine requestLine = parseRequestLine(reader); + HttpRequestHeaders headers = parseHeaders(reader); + HttpRequestBody body = parseBody(reader, headers.getContentLength()); + + return new HttpRequest(requestLine, headers, body); + } + + private static HttpRequestLine parseRequestLine(BufferedReader reader) throws IOException { + String requestLine = reader.readLine(); + if (requestLine == null) { + throw new IllegalArgumentException("요청에 Reqeust-line이 없습니다."); + } + return HttpRequestLine.from(requestLine); + } + + private static HttpRequestHeaders parseHeaders(BufferedReader reader) throws IOException { + Map headers = new HashMap<>(); + + String headerLine; + while ((headerLine = reader.readLine()) != null && !headerLine.isEmpty()) { + int indexOfDelimiter = getIndexOfDelimiter(headerLine); + + String property = headerLine.substring(0, indexOfDelimiter); + String value = headerLine.substring(indexOfDelimiter + START_AFTER_SPACE); + headers.put(property, value); + } + + return new HttpRequestHeaders(headers); + } + + private static int getIndexOfDelimiter(String headerLine) { + int indexOfDelimiter = headerLine.indexOf(DELIMITER); + + if (indexOfDelimiter == -1) { + throw new IllegalArgumentException("요청의 헤더가 잘못되었습니다."); + } + return indexOfDelimiter; + } + + private static HttpRequestBody parseBody(BufferedReader reader, int contentLength) throws IOException { + if (contentLength == 0) { + return new HttpRequestBody(new HashMap<>()); + } + + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + String requestBody = new String(buffer); + + Map body = parse(requestBody); + + if (body.size() != requestBody.split(PARAMS_DELIMITER).length) { + throw new IllegalArgumentException("요청의 바디가 잘못되었습니다."); + } + + return new HttpRequestBody(body); + } + + private static Map parse(String requestBody) { + return Arrays.stream(requestBody.split(PARAMS_DELIMITER)) + .map(property -> property.split(PARAM_VALUE_DELIMITER)) + .filter(propertyAndValue -> propertyAndValue.length == 2) + .collect(Collectors.toMap(data -> data[PROPERTY_INDEX], data -> data[VALUE_INDEX])); + } + + private HttpRequestParser() { + } +} 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..6a73c86359 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,96 @@ +package org.apache.coyote.http11.response; + +import common.http.ContentType; +import common.http.Cookie; +import common.http.HttpStatus; +import common.http.Response; + +import static org.apache.Constants.CRLF; + +public class HttpResponse implements Response { + + private final HttpStatusLine httpStatusLine; + private final HttpResponseHeaders httpResponseHeaders; + private final HttpResponseBody httpResponseBody; + + public HttpResponse() { + this.httpStatusLine = new HttpStatusLine(); + this.httpResponseHeaders = new HttpResponseHeaders(); + this.httpResponseBody = new HttpResponseBody(); + } + + @Override + public void addVersionOfTheProtocol(String versionOfTheProtocol) { + httpStatusLine.addVersionOfTheProtocol(versionOfTheProtocol); + } + + @Override + public void addHttpStatus(HttpStatus httpStatus) { + httpStatusLine.addHttpStatus(httpStatus); + } + + @Override + public void addContentType(ContentType contentType) { + httpResponseHeaders.addHeaderFieldAndValue("Content-Type", contentType.getType() + ";charset=utf-8"); + } + + @Override + public void addCookie(Cookie cookie) { + httpResponseHeaders.addHeaderFieldAndValue("Set-Cookie", cookie.getValue()); + } + + @Override + public void sendRedirect(String redirectURL) { + httpResponseHeaders.addHeaderFieldAndValue("Location", redirectURL); + } + + @Override + public void addStaticResourcePath(String name) { + httpResponseHeaders.addHeaderFieldAndValue("Resource-Path", name); + } + + @Override + public void addBody(String body) { + httpResponseHeaders.addHeaderFieldAndValue("Content-Length", String.valueOf(body.getBytes().length)); + httpResponseBody.addBody(body); + } + + @Override + public boolean hasStaticResourcePath() { + return httpResponseHeaders.hasStaticResourcePath(); + } + + @Override + public String getStaticResourcePath() { + return httpResponseHeaders.getStaticResourcePath(); + } + + @Override + public void addException(Exception e) { + httpResponseHeaders.addException(e); + } + + @Override + public String getException() { + return httpResponseHeaders.getException(); + } + + @Override + public boolean hasException() { + return httpResponseHeaders.hasException(); + } + + public String getMessage() { + if (httpResponseBody.exist()) { + httpResponseBody.validateLength(httpResponseHeaders.getContentLength()); + return String.join(CRLF, + httpStatusLine.getMessage(), + httpResponseHeaders.getMessage(), + httpResponseBody.getMessage()); + } + + return String.join(CRLF, + httpStatusLine.getMessage(), + httpResponseHeaders.getMessage()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseBody.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseBody.java new file mode 100644 index 0000000000..f36d536b10 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseBody.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.response; + +class HttpResponseBody { + + private String body; + + HttpResponseBody() { + } + + void addBody(String body) { + this.body = body; + } + + void validateLength(long length) { + if (length != body.getBytes().length) { + throw new IllegalArgumentException("Response Body의 길이가 유효하지 않습니다."); + } + } + + boolean exist() { + return body != null; + } + + public String getMessage() { + return body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java new file mode 100644 index 0000000000..3fdb9308e2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java @@ -0,0 +1,80 @@ +package org.apache.coyote.http11.response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.Constants.CRLF; +import static org.apache.Constants.SPACE; + +class HttpResponseHeaders { + + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String LOCATION = "Location"; + public static final String RESOURCE_PATH = "Resource-Path"; + + private final Map> headers; + + HttpResponseHeaders() { + this.headers = new HashMap<>(); + } + + void addHeaderFieldAndValue(String field, String value) { + Set uniqueHeaderFields = Set.of(CONTENT_TYPE, CONTENT_LENGTH, LOCATION); + + if (headers.containsKey(field) && uniqueHeaderFields.contains(field)) { + throw new IllegalStateException(String.format("헤더에 이미 %s에 대한 값이 존재합니다.", field)); + } + + if (headers.containsKey(field) && !uniqueHeaderFields.contains(field)) { + headers.get(field).add(value); + return; + } + + headers.put(field, List.of(value)); + } + + public void addException(Exception e) { + headers.put("Exception-Type", List.of(e.getClass().getName())); + } + + boolean hasStaticResourcePath() { + return headers.containsKey(RESOURCE_PATH); + } + + public boolean hasException() { + return headers.containsKey("Exception-Type"); + } + + String getStaticResourcePath() { + return headers.get(RESOURCE_PATH).get(0); + } + + public long getContentLength() { + return Long.parseLong(headers.get(CONTENT_LENGTH).get(0)); + } + + public String getException() { + return headers.get("Exception-Type").get(0); + } + + public String getMessage() { + StringBuilder stringBuilder = new StringBuilder(); + + for (Map.Entry> fieldAndValue : headers.entrySet()) { + for (String value : fieldAndValue.getValue()) { + stringBuilder + .append(fieldAndValue.getKey()) + .append(":") + .append(SPACE) + .append(value) + .append(SPACE) + .append(CRLF); + } + } + + return stringBuilder.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusLine.java new file mode 100644 index 0000000000..9b40b16e36 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusLine.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11.response; + +import common.http.HttpStatus; + +import static org.apache.Constants.SPACE; + +class HttpStatusLine { + + private String versionOfTheProtocol; + private HttpStatus httpStatus; + + HttpStatusLine() {} + + void addVersionOfTheProtocol(String versionOfTheProtocol) { + if (this.versionOfTheProtocol != null) { + throw new IllegalStateException("프로토콜의 버전이 이미 존재합니다."); + } + + this.versionOfTheProtocol = versionOfTheProtocol; + } + + void addHttpStatus(HttpStatus httpStatus) { + if (this.httpStatus != null) { + throw new IllegalStateException("Http Status가 이미 존재합니다."); + } + + this.httpStatus = httpStatus; + } + + public String getMessage() { + return versionOfTheProtocol + + SPACE + + httpStatus.getStatusCode() + + SPACE + + httpStatus.getStatusMessage() + + SPACE; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/session/HttpCookie.java deleted file mode 100644 index 1bf7101cf8..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/HttpCookie.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.apache.coyote.http11.session; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class HttpCookie { - - private static final String JSESSIONID = "JSESSIONID"; - private static final String SEPARATOR = "; "; - private static final String DELIMITER = "="; - - private static final int NAME_INDEX = 0; - private static final int VALUE_INDEX = 1; - - private final Map items; - - private HttpCookie(Map items) { - this.items = items; - } - - public static HttpCookie from(String cookieHeader) { - if (cookieHeader == null || cookieHeader.isEmpty()) { - return new HttpCookie(new HashMap<>()); - } - return new HttpCookie(parse(cookieHeader)); - } - - private static Map parse(String values) { - Map items = new HashMap<>(); - String[] nameAndValues = values.split(SEPARATOR); - for (String cookie : nameAndValues) { - String[] nameAndValue = cookie.split(DELIMITER); - items.put(nameAndValue[NAME_INDEX], nameAndValue[VALUE_INDEX]); - } - return items; - } - - public boolean hasJSessionId() { - return items.containsKey(JSESSIONID); - } - - public void bake() { - items.put(JSESSIONID, UUID.randomUUID().toString()); - } - - public String getJSessionId() { - return items.get(JSESSIONID); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java deleted file mode 100644 index e7765a6fbf..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.apache.coyote.http11.session; - -import java.util.HashMap; -import java.util.Map; - -public class SessionManager implements Manager { - - private static final Map SESSIONS = new HashMap<>(); - - @Override - public void add(Session session) { - SESSIONS.put(session.getId(), session); - } - - @Override - public Session findSession(String id) { - return SESSIONS.get(id); - } - - @Override - public void remove(Session session) { - SESSIONS.remove(session.getId()); - } -} diff --git a/tomcat/src/main/resources/static/401.html b/tomcat/src/main/resources/static/401.html index 444019ac4e..902b7707df 100644 --- a/tomcat/src/main/resources/static/401.html +++ b/tomcat/src/main/resources/static/401.html @@ -1,52 +1,54 @@ - - - - - - - 404 Error - SB Admin - - - - -
-
-
-
-
-
-
-

401

-

Unauthorized

-

Access to this resource is denied.

- - - Return to Dashboard - -
-
+ + + + + + + 401 Error - SB Admin + + + + +
+
+
+
+
+
+
+

401

+

Unauthorized

+

Access to this resource is denied.

+ + + Return to Dashboard +
-
+
-
+
+
- - - - + + + + + + diff --git a/tomcat/src/main/resources/static/404.html b/tomcat/src/main/resources/static/404.html index 980e9ec7f0..e16472ebf7 100644 --- a/tomcat/src/main/resources/static/404.html +++ b/tomcat/src/main/resources/static/404.html @@ -1,51 +1,53 @@ - - - - - - - 404 Error - SB Admin - - - - -
-
-
-
-
-
-
- -

This requested URL was not found on this server.

- - - Return to Dashboard - -
-
+ + + + + + + 404 Error - SB Admin + + + + +
+
+
+
+
+
+
+ +

This requested URL was not found on this server.

+ + + Return to Dashboard +
-
+
-
+
+
- - - - + + + + + + diff --git a/tomcat/src/main/resources/static/409.html b/tomcat/src/main/resources/static/409.html new file mode 100644 index 0000000000..40b074c1fd --- /dev/null +++ b/tomcat/src/main/resources/static/409.html @@ -0,0 +1,53 @@ + + + + + + + + + 409 Error - SB Admin + + + + +
+
+
+
+
+
+
+

409

+

Conflict

+ + + Return to Dashboard + +
+
+
+
+
+
+ +
+ + + + diff --git a/tomcat/src/main/resources/static/500.html b/tomcat/src/main/resources/static/500.html index f786af3509..854122eb7b 100644 --- a/tomcat/src/main/resources/static/500.html +++ b/tomcat/src/main/resources/static/500.html @@ -1,51 +1,53 @@ - - - - - - - 404 Error - SB Admin - - - - -
-
-
-
-
-
-
-

500

-

Internal Server Error

- - - Return to Dashboard - -
-
+ + + + + + + 404 Error - SB Admin + + + + +
+
+
+
+
+
+
+

500

+

Internal Server Error

+ + + Return to Dashboard +
-
+
-
+
+
- - - - + + + + + + diff --git a/tomcat/src/test/java/common/http/ContentTypeTest.java b/tomcat/src/test/java/common/http/ContentTypeTest.java new file mode 100644 index 0000000000..77f8ebe36f --- /dev/null +++ b/tomcat/src/test/java/common/http/ContentTypeTest.java @@ -0,0 +1,70 @@ +package common.http; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ContentTypeTest { + + @Test + void 확장자명으로_Content_Type을_찾는다() { + // given + String html = "html"; + String css = "css"; + String js = "js"; + String ico = "ico"; + + // expect + assertThat(ContentType.findByExtension(html)).isEqualTo(ContentType.HTML); + assertThat(ContentType.findByExtension(css)).isEqualTo(ContentType.CSS); + assertThat(ContentType.findByExtension(js)).isEqualTo(ContentType.JS); + assertThat(ContentType.findByExtension(ico)).isEqualTo(ContentType.ICO); + } + + @Test + void 유효한_확장자명이_아니면_예외를_반환한다() { + // given + String avi = "avi"; + + // expect + assertThatThrownBy(() -> ContentType.findByExtension(avi)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 확장자명 입니다."); + } + + @Test + void 경로로_Content_Type을_찾는다 () { + // given + String path = "/index.html"; + + // expect + assertThat(ContentType.findByPath(path)).isEqualTo(ContentType.HTML); + } + + @Test + void 경로에_확장자가_없으면_예외를_반환한다() { + // given + String path = "/로이스의은밀한사생활"; + + // expect + assertThatThrownBy(() -> ContentType.findByPath(path)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("파일의 확장자명이 없습니다."); + } + + @Test + void 경로의_확장자가_유효하지_않으면_예외를_반환한다() { + // given + String path = "/로이스의은밀한사생활.avi"; + + // expect + assertThatThrownBy(() -> ContentType.findByPath(path)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 확장자명 입니다."); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/session/HttpCookieTest.java b/tomcat/src/test/java/common/http/CookieTest.java similarity index 70% rename from tomcat/src/test/java/org/apache/coyote/http11/session/HttpCookieTest.java rename to tomcat/src/test/java/common/http/CookieTest.java index 27902d25ec..130f6663dd 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/session/HttpCookieTest.java +++ b/tomcat/src/test/java/common/http/CookieTest.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11.session; +package common.http; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayNameGeneration; @@ -7,7 +7,7 @@ @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class HttpCookieTest { +class CookieTest { @Test void Cookie_헤더로부터_Cookie를_생성한다() { @@ -15,9 +15,9 @@ class HttpCookieTest { String cookieHeader = "yummy_cookie=choco; tasty_cookie=strawberry; JSESSIONID=656cef62-e3c4-40bc-a8df-94732920ed46"; // when - HttpCookie cookie = HttpCookie.from(cookieHeader); + Cookie cookie = Cookie.from(cookieHeader); // then - Assertions.assertThat(cookie.getJSessionId()).isEqualTo("656cef62-e3c4-40bc-a8df-94732920ed46"); + Assertions.assertThat(cookie.getAttribute("JSESSIONID")).isEqualTo("656cef62-e3c4-40bc-a8df-94732920ed46"); } } diff --git a/tomcat/src/test/java/common/http/CookiesTest.java b/tomcat/src/test/java/common/http/CookiesTest.java new file mode 100644 index 0000000000..3afe71a1a9 --- /dev/null +++ b/tomcat/src/test/java/common/http/CookiesTest.java @@ -0,0 +1,24 @@ +package common.http; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CookiesTest { + + @Test + void 세션_아이디로_쿠키를_생성한다() { + // given + String id = "sessionid"; + + // when + Cookie cookie = Cookies.ofJSessionId(id); + + // then + assertThat(cookie.getAttribute("JSESSIONID")).isEqualTo(id); + } +} diff --git a/tomcat/src/test/java/common/http/HttpMethodTest.java b/tomcat/src/test/java/common/http/HttpMethodTest.java new file mode 100644 index 0000000000..642cb7dab9 --- /dev/null +++ b/tomcat/src/test/java/common/http/HttpMethodTest.java @@ -0,0 +1,32 @@ +package common.http; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpMethodTest { + @Test + void 메서드_이름으로_Http_Method를_가져온다() { + // given + String get = "GET"; + + // expect + assertThat(HttpMethod.parseHttpMethod(get)).isEqualTo(HttpMethod.GET); + } + + @Test + void 유효하지_않은_메서드_이름이면_예외를_반환한다() { + // given + String 가져온나 = "가져온나"; + + // expect + Assertions.assertThatThrownBy(() -> HttpMethod.parseHttpMethod(가져온나)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 HTTP 메서드입니다."); + } +} diff --git a/tomcat/src/test/java/common/http/SessionTest.java b/tomcat/src/test/java/common/http/SessionTest.java new file mode 100644 index 0000000000..93abfaae80 --- /dev/null +++ b/tomcat/src/test/java/common/http/SessionTest.java @@ -0,0 +1,41 @@ +package common.http; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SessionTest { + + @Test + void 세션의_속성을_가져온다() { + // given + Session session = new Session("id"); + session.setAttribute("user", "로이스"); + + // when + Object value = session.getAttribute("user"); + + // then + assertThat(value).isEqualTo("로이스"); + } + + @Test + void 세션에서_속성을_삭제한다() { + // given + Session session = new Session("id"); + session.setAttribute("user", "로이스"); + session.setAttribute("reviewee", "리오"); + + // when + session.removeAttribute("user"); + + // then + assertThat(session.getAttribute("user")).isNull(); + assertThat(session.getAttribute("reviewee")).isEqualTo("리오"); + } + +} 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..75d1838ff9 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/HomeControllerTest.java @@ -0,0 +1,37 @@ +package nextstep.jwp.controller; + +import common.http.Request; +import common.http.Response; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.apache.Constants.CRLF; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HomeControllerTest { + + @Test + void 응답에_속성을_추가한다() { + // given + HomeController homeController = new HomeController(); + Request request = mock(HttpRequest.class); + Response httpResponse = new HttpResponse(); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + // when + homeController.doGet(request, httpResponse); + + // then + Assertions.assertThat(httpResponse.getMessage()).hasToString( + "HTTP/1.1 200 OK " + CRLF + + "Content-Length: 12 " + CRLF + + "Content-Type: text/html;charset=utf-8 " + CRLF + CRLF + + "Hello world!"); + } +} 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..95e639f6cd --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java @@ -0,0 +1,121 @@ +package nextstep.jwp.controller; + +import common.http.Request; +import common.http.Response; +import common.http.Session; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.startup.SessionManager; +import org.apache.coyote.http11.request.HttpRequest; +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; + +import static org.apache.Constants.CRLF; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class LoginControllerTest { + + @Test + void GET요청시_세션이_존재하는_경우_indexhtml로_리다이렉션을_한다 () { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + Session session = new Session("id"); + SessionManager sessionManager = mock(SessionManager.class); + LoginController loginController = new LoginController(); + + when(request.getSession()).thenReturn(session); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + when(sessionManager.findSession(any())).thenReturn(session); + when(request.hasValidSession()).thenReturn(true); + + // when + loginController.doGet(request, response); + + // then + assertThat(response.getMessage()).hasToString( + "HTTP/1.1 302 Found " + CRLF + + "Set-Cookie: JSESSIONID=id " + CRLF + + "Location: /index.html " + CRLF + ); + } + + @Test + void GET요청시_세션이_존재하지_않는_경우_loginhtml을_응답에_실어준다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + LoginController loginController = new LoginController(); + + // when + loginController.doGet(request, response); + + // then + assertThat(response.getStaticResourcePath()).isEqualTo("/login.html"); + } + + @Test + void POST요청시_회원_정보가_없으면_예외를_반환한다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + LoginController loginController = new LoginController(); + + when(request.getAccount()).thenReturn("롤스로이스"); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + + // expect + assertThatThrownBy(() -> loginController.doPost(request, response)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("회원 정보가 존재하지 않습니다."); + } + + @Test + void POST요청시_비밀번호가_틀리면_401html로_리다이렉트한다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + LoginController loginController = new LoginController(); + InMemoryUserRepository.save(new User("로이스", "잘생김", "내마음속")); + when(request.getAccount()).thenReturn("로이스"); + when(request.getPassword()).thenReturn("못생김"); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + + // when + loginController.doPost(request, response); + + // then + assertThat(response.getStaticResourcePath()).isEqualTo("/401.html"); + } + + @Test + void POST요청시_로그인에_성공하면_indexhtml로_리다이렉트한다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + LoginController loginController = new LoginController(); + InMemoryUserRepository.save(new User("로이스", "잘생김", "내마음속")); + when(request.getAccount()).thenReturn("로이스"); + when(request.getPassword()).thenReturn("잘생김"); + when(request.getSession(true)).thenReturn(new Session("로이스")); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + + // when + loginController.doPost(request, response); + + // then + assertThat(response.getMessage()).hasToString( + "HTTP/1.1 302 Found " + CRLF + + "Set-Cookie: JSESSIONID=로이스 " + CRLF + + "Location: /index.html " + CRLF + ); + } +} 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..7e7b336b84 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java @@ -0,0 +1,76 @@ +package nextstep.jwp.controller; + +import common.http.Request; +import common.http.Response; +import common.http.Session; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.request.HttpRequest; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RegisterControllerTest { + + @Test + void GET요청시_registerhtml의_경로를_응답에_실는다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + RegisterController registerController = new RegisterController(); + + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + + // when + registerController.doGet(request, response); + + // then + assertThat(response.getStaticResourcePath()).isEqualTo("/register.html"); + } + + @Test + void POST요청시_이미_가입된_회원이면_예외를_반환한다() { + // given + InMemoryUserRepository.save(new User("로이스", "잘생김", "내마음속")); + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + RegisterController registerController = new RegisterController(); + + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + when(request.getAccount()).thenReturn("로이스"); + + // expect + assertThatThrownBy(() -> registerController.doPost(request, response)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 가입된 회원입니다."); + } + + @Test + void POST요청시_회원가입을_진행한다() { + // given + Request request = mock(HttpRequest.class); + Response response = new HttpResponse(); + RegisterController registerController = new RegisterController(); + + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + when(request.getAccount()).thenReturn("롤스로이스"); + when(request.getPassword()).thenReturn("비쌈"); + when(request.getEmail()).thenReturn("근데내꺼ㅎ"); + when(request.getSession(true)).thenReturn(new Session("id")); + // when + registerController.doPost(request, response); + + // then + User user = InMemoryUserRepository.findByAccount("롤스로이스").get(); + assertThat(user.checkPassword("비쌈")).isTrue(); + assertThat(response.getMessage()).contains("Location: /index.html"); + } +} diff --git a/tomcat/src/test/java/org/apache/catalina/startup/DynamicControllerManagerTest.java b/tomcat/src/test/java/org/apache/catalina/startup/DynamicControllerManagerTest.java new file mode 100644 index 0000000000..3ec575e43c --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/startup/DynamicControllerManagerTest.java @@ -0,0 +1,40 @@ +package org.apache.catalina.startup; + +import common.http.HttpMethod; +import common.http.Request; +import common.http.Session; +import nextstep.jwp.controller.LoginController; +import org.apache.coyote.http11.request.HttpRequest; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DynamicControllerManagerTest { + + @Test + void 요청의_첫_줄_정보로_controller를_찾아서_실행시킨다() { + // given + DynamicControllerManager dynamicControllerManager = new DynamicControllerManager(); + dynamicControllerManager.add("/login", new LoginController()); + Request request = mock(HttpRequest.class); + HttpResponse response = new HttpResponse(); + Session session = new Session("id"); + when(request.getPath()).thenReturn("/login"); + when(request.getSession(true)).thenReturn(session); + when(request.getSession()).thenReturn(session); + when(request.getHttpMethod()).thenReturn(HttpMethod.GET); + + // when + dynamicControllerManager.service(request, response); + + // then + assertThat(response.getStaticResourcePath()).isEqualTo("/login.html"); + } +} diff --git a/tomcat/src/test/java/org/apache/catalina/startup/SessionManagerTest.java b/tomcat/src/test/java/org/apache/catalina/startup/SessionManagerTest.java new file mode 100644 index 0000000000..01362be76c --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/startup/SessionManagerTest.java @@ -0,0 +1,40 @@ +package org.apache.catalina.startup; + +import common.http.Session; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SessionManagerTest { + + @Test + void Session을_추가한다() { + // given + Session session = new Session("로이스"); + SessionManager sessionManager = new SessionManager(); + + // when + sessionManager.add(session); + + // then + assertThat(sessionManager.findSession("로이스")).isEqualTo(session); + } + + @Test + void Sessoin을_삭제한다() { + // given + Session session = new Session("로이스"); + SessionManager sessionManager = new SessionManager(); + sessionManager.add(session); + + // when + sessionManager.remove(session); + + // then + assertThat(sessionManager.hasSession(session)).isFalse(); + } +} 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 2aba8c56e0..cada36b45a 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,5 +1,9 @@ package org.apache.coyote.http11; +import common.http.ControllerManager; +import nextstep.jwp.controller.HomeController; +import org.apache.catalina.startup.DynamicControllerManager; +import org.apache.coyote.Processor; import org.junit.jupiter.api.Test; import support.StubSocket; @@ -7,6 +11,7 @@ import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -15,21 +20,24 @@ class Http11ProcessorTest { @Test void process() { // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final StubSocket socket = new StubSocket(); + final ControllerManager controllerManager = new DynamicControllerManager(); + controllerManager.add("/", new HomeController()); + final Processor processor = new Http11Processor(socket, controllerManager); // when processor.process(socket); // then - var expected = String.join("\r\n", + var expected = List.of("\r\n", "HTTP/1.1 200 OK ", "Content-Type: text/html;charset=utf-8 ", "Content-Length: 12 ", "", - "Hello world!"); + "Hello world!" + ); - assertThat(socket.output()).isEqualTo(expected); + assertThat(socket.output()).contains(expected); } @Test @@ -43,19 +51,20 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final var controllerManager = new DynamicControllerManager(); + final Http11Processor processor = new Http11Processor(socket, controllerManager); // when processor.process(socket); // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + var expected = List.of("HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: 5564 \r\n", + "\r\n", + new String(Files.readAllBytes(new File(resource.getFile()).toPath()))); - assertThat(socket.output()).isEqualTo(expected); + assertThat(socket.output()).contains(expected); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestFirstLineInfoTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestFirstLineInfoTest.java deleted file mode 100644 index 1f13c919de..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestFirstLineInfoTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.apache.coyote.http11; - -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class HttpRequestFirstLineInfoTest { - - @Test - void HTTP_요청의_첫_줄의_정보를_읽는다() { - // given - final String firstLine = "GET /index.html HTTP/1.1 "; - - // when - HttpRequestFirstLineInfo infos = HttpRequestFirstLineInfo.from(firstLine); - - // then - assertSoftly(softly -> { - softly.assertThat(infos.getHttpMethod()).isEqualTo(HttpMethod.GET); - softly.assertThat(infos.getUri()).isEqualTo("/index.html"); - softly.assertThat(infos.getVersionOfTheProtocol()).isEqualTo("HTTP/1.1"); - }); - } - - @Test - void 첫_줄이_유효하지_않으면_예외를_발생시킨다() { - // given - final String firstLine = "GET HTTP/1.1 "; - - // expect - assertThatThrownBy(() -> HttpRequestFirstLineInfo.from(firstLine)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("유효하지 않은 Request-line 입니다."); - } - - @Test - void HTTP_메서드가_유효하지_않으면_예외를_발생시킨다() { - // given - final String firstLine = "내놔라 /index.html HTTP/1.1 "; - - // expect - assertThatThrownBy(() -> HttpRequestFirstLineInfo.from(firstLine)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("유효하지 않은 HTTP 메서드입니다."); - } -} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpResponseMakerTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpResponseMakerTest.java deleted file mode 100644 index 5bd75f51ba..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/HttpResponseMakerTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.apache.coyote.http11; - -import nextstep.jwp.db.InMemoryUserRepository; -import nextstep.jwp.model.User; -import org.apache.coyote.http11.session.SessionManager; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.io.IOException; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class HttpResponseMakerTest { - - @Mock - private HttpRequestParser mockParser; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - void 로그인_성공_시에_요청에_해당하는_응답을_반환한다() throws IOException { - // given - when(mockParser.getHttpRequestFirstLineInfo()).thenReturn( - HttpRequestFirstLineInfo.from("POST /login HTTP/1.1")); - when(mockParser.getBody()) - .thenReturn( - Map.of( - "account", "testuser", - "password", "password123", - "email", "email") - ); - - InMemoryUserRepository.save(new User("testuser", "password123", "email")); - - // when - String response = HttpResponseMaker.makeFrom(mockParser, new SessionManager()); - - // then - assertTrue(response.contains("Location: /index.html")); - } - - @Test - void 로그인_실패_시에_요청에_해당하는_응답을_반환한다() throws IOException { - // given - when(mockParser.getHttpRequestFirstLineInfo()).thenReturn( - HttpRequestFirstLineInfo.from("POST /login HTTP/1.1")); - when(mockParser.getBody()) - .thenReturn( - Map.of( - "account", "testuser", - "password", "password456", - "email", "email") - ); - - InMemoryUserRepository.save(new User("testuser", "password123", "email")); - - // when - String response = HttpResponseMaker.makeFrom(mockParser, new SessionManager()); - - // then - assertTrue(response.contains("Location: /401.html")); - } - - @Test - void 회원_정보가_없을_시에_예외를_발생시킨다() { - // given - when(mockParser.getHttpRequestFirstLineInfo()).thenReturn( - HttpRequestFirstLineInfo.from("POST /login HTTP/1.1")); - when(mockParser.getBody()) - .thenReturn( - Map.of( - "account", "test", - "password", "password123", - "email", "email") - ); - - InMemoryUserRepository.save(new User("testuser", "password123", "email")); - - // expect - SessionManager sessionManager = new SessionManager(); - Assertions.assertThatThrownBy(() -> HttpResponseMaker.makeFrom(mockParser, sessionManager)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("회원 정보가 존재하지 않습니다."); - } -} - diff --git a/tomcat/src/test/java/org/apache/coyote/http11/RequestMapperTest.java b/tomcat/src/test/java/org/apache/coyote/http11/RequestMapperTest.java deleted file mode 100644 index 877a3b4697..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/RequestMapperTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.apache.coyote.http11; - -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class RequestMapperTest { - - @Test - void 요청의_첫_줄_정보로_request_mapper를_찾는다() { - // given - HttpRequestFirstLineInfo info = HttpRequestFirstLineInfo.from("GET /index.html HTTP/1.1"); - - // when - RequestMapper mapper = RequestMapper.findMapper(info); - - // then - assertThat(mapper).isSameAs(RequestMapper.INDEX); - - } - - @Test - void 요청의_첫_줄_정보로_request_mapper를_찾을_수_없으면_예외를_발생시킨다() { - // given - HttpRequestFirstLineInfo info = HttpRequestFirstLineInfo.from("POST /index.html HTTP/1.1"); - - // expect - assertThatThrownBy(() -> RequestMapper.findMapper(info)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("잘못된 요청입니다."); - } -} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticControllerManagerTest.java b/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticControllerManagerTest.java new file mode 100644 index 0000000000..ad05c65da8 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticControllerManagerTest.java @@ -0,0 +1,49 @@ +package org.apache.coyote.http11.controller; + +import common.http.HttpMethod; +import common.http.Request; +import nextstep.jwp.controller.LoginController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class StaticControllerManagerTest { + + @Test + void 컨트롤러를_추가할_시_예외를_반환한다() { + // given + StaticControllerManager staticControllerManager = StaticControllerManager.getInstance(); + LoginController loginController = new LoginController(); + + // expect + Assertions.assertThatThrownBy(() -> staticControllerManager.add("/login", loginController)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("컨트롤러를 추가할 수 없습니다."); + } + + @Test + void 정적파일을_제공하는_컨트롤러를_찾아서_service를_실행한다() { + // given + StaticControllerManager staticControllerManager = StaticControllerManager.getInstance(); + Request request = mock(HttpRequest.class); + HttpResponse response = new HttpResponse(); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + when(request.getPath()).thenReturn("/login.html"); + when(request.getHttpMethod()).thenReturn(HttpMethod.GET); + + // when + staticControllerManager.service(request, response); + + // then + assertThat(response.getMessage()).contains("HTTP/1.1 200 OK"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticResourceControllerTest.java b/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticResourceControllerTest.java new file mode 100644 index 0000000000..f76e8cadf2 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/controller/StaticResourceControllerTest.java @@ -0,0 +1,55 @@ +package org.apache.coyote.http11.controller; + +import common.http.HttpMethod; +import common.http.HttpStatus; +import common.http.Request; +import org.apache.coyote.http11.request.HttpRequest; +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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class StaticResourceControllerTest { + + @Test + void 요청에_정적리소스_경로가_있을_경우_정적리소스를_제공한다() { + // given + Request request = mock(HttpRequest.class); + HttpResponse response = new HttpResponse(); + when(request.getVersionOfTheProtocol()).thenReturn("HTTP/1.1"); + when(request.getPath()).thenReturn("/login.html"); + when(request.getHttpMethod()).thenReturn(HttpMethod.GET); + + StaticResourceController staticResourceController = new StaticResourceController(); + + // when + staticResourceController.service(request, response); + + // then + assertThat(response.getMessage()).contains("HTTP/1.1 200 OK"); + } + + @Test + void 응답에_정적리소스_경로가_있을_경우_정적리소스를_제공한다() { + // given + Request request = mock(HttpRequest.class); + HttpResponse response = new HttpResponse(); + response.addVersionOfTheProtocol("HTTP/1.1"); + response.addStaticResourcePath("/login.html"); + response.addHttpStatus(HttpStatus.OK); + + StaticResourceController staticResourceController = new StaticResourceController(); + + // when + staticResourceController.service(request, response); + + // then + assertThat(response.getMessage()).contains("HTTP/1.1 200 OK"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestParserTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java similarity index 61% rename from tomcat/src/test/java/org/apache/coyote/http11/HttpRequestParserTest.java rename to tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java index 6b80ce00ce..4c6dced219 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestParserTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java @@ -1,5 +1,6 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.request; +import common.http.HttpMethod; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -18,8 +19,8 @@ class HttpRequestParserTest { @Test void HTTP_요청을_파싱한다() throws IOException { // given - final String httpRequest = String.join("\r\n", - "GET /index.html HTTP/1.1 ", + final String request = String.join("\r\n", + "POST /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", "Content-Length: 80", @@ -27,34 +28,18 @@ class HttpRequestParserTest { "", "account=gugu&password=password&email=hkkang%40woowahan.com"); - BufferedReader reader = new BufferedReader(new StringReader(httpRequest)); + BufferedReader reader = new BufferedReader(new StringReader(request)); // when - HttpRequestParser requestParser = HttpRequestParser.from(reader); + HttpRequest parsed = HttpRequestParser.parse(reader); // then assertSoftly(softly -> { - softly.assertThat(requestParser) - .isNotNull(); - - softly.assertThat(requestParser.getHttpRequestFirstLineInfo()) - .isNotNull() - .extracting("httpMethod", "uri", "versionOfTheProtocol") - .containsExactly(HttpMethod.GET, "/index.html", "HTTP/1.1"); - - softly.assertThat(requestParser.getHeaders()) - .isNotNull() - .hasSize(4) - .containsEntry("Host", "localhost:8080 ") - .containsEntry("Connection", "keep-alive ") - .containsEntry("Content-Length", "80") - .containsEntry("Content-Type", "application/x-www-form-urlencoded"); - - softly.assertThat(requestParser.getBody()) - .isNotNull() - .hasSize(3) - .containsEntry("account", "gugu") - .containsEntry("password", "password"); + softly.assertThat(parsed).isNotNull(); + softly.assertThat(parsed.getHttpMethod()).isEqualTo(HttpMethod.POST); + softly.assertThat(parsed.getVersionOfTheProtocol()).isEqualTo("HTTP/1.1"); + softly.assertThat(parsed.getAccount()).isEqualTo("gugu"); + softly.assertThat(parsed.getPassword()).isEqualTo("password"); }); reader.close(); @@ -68,8 +53,8 @@ class HttpRequestParserTest { BufferedReader reader = new BufferedReader(new StringReader(httpRequest)); // expect - assertThatThrownBy(() -> HttpRequestParser.from(reader)) - .isInstanceOf(IOException.class) + assertThatThrownBy(() -> HttpRequestParser.parse(reader)) + .isInstanceOf(IllegalArgumentException.class) .hasMessage("요청에 Reqeust-line이 없습니다."); reader.close(); @@ -90,7 +75,7 @@ class HttpRequestParserTest { BufferedReader reader = new BufferedReader(new StringReader(httpRequest)); // expect - assertThatThrownBy(() -> HttpRequestParser.from(reader)) + assertThatThrownBy(() -> HttpRequestParser.parse(reader)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("요청의 바디가 잘못되었습니다."); 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..8f26eb18d4 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.response; + +import common.http.ContentType; +import common.http.Cookie; +import common.http.HttpStatus; +import org.assertj.core.api.Assertions; +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 HttpResponse를_String으로_변환한다() { + // given + HttpResponse httpResponse = new HttpResponse(); + httpResponse.addVersionOfTheProtocol("HTTP/1.1"); + httpResponse.addHttpStatus(HttpStatus.OK); + httpResponse.addCookie(Cookie.from("JSESSIONID=1234")); + httpResponse.addContentType(ContentType.HTML); + httpResponse.addBody("로이스 잘생겼다"); + + // when + String string = httpResponse.getMessage(); + + // then + Assertions.assertThat(string).contains("HTTP/1.1 200 OK", "Set-Cookie: JSESSIONID=1234", "Content-Type: text/html", "로이스 잘생겼다"); + } + +}