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..9cbf1595aa 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,21 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + final WebContentInterceptor interceptor = new WebContentInterceptor(); + final CacheControl cacheControl = CacheControl + .noCache() + .cachePrivate(); + interceptor.addCacheMapping(cacheControl, "/**"); + registry.addInterceptor(interceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..0ad869edd0 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,21 @@ 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; + +import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES; @Configuration public class EtagFilterConfiguration { -// @Bean -// public FilterRegistrationBean shallowEtagHeaderFilter() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + + FilterRegistrationBean filterFilterRegistrationBean + = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterFilterRegistrationBean.addUrlPatterns("/etag", PREFIX_STATIC_RESOURCES + "/*"); + return filterFilterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..5a422e5b2e 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -2,9 +2,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.time.Duration; + @Configuration public class CacheBustingWebConfig implements WebMvcConfigurer { @@ -20,6 +23,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") - .addResourceLocations("classpath:/static/"); + .addResourceLocations("classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..522113ace8 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -2,6 +2,9 @@ handlebars: suffix: .html server: + compression: + enabled: true + min-response-size: 10 tomcat: accept-count: 1 max-connections: 1 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/tomcat/build.gradle b/tomcat/build.gradle index 5e2a76a777..1cae356544 100644 --- a/tomcat/build.gradle +++ b/tomcat/build.gradle @@ -21,5 +21,6 @@ dependencies { testImplementation "org.assertj:assertj-core:3.24.2" testImplementation "org.mockito:mockito-core:5.4.0" testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.2" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } 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..48acdb2147 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,68 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.controller.HttpController; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.util.*; + +import static org.apache.coyote.http11.common.HttpHeaderType.LOCATION; +import static org.apache.coyote.http11.common.HttpHeaderType.SET_COOKIE; +import static org.apache.coyote.http11.response.HttpStatusCode.FOUND; + +public class LoginController extends HttpController { + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final Set requestType = Set.of("/login"); + return requestType.contains(httpRequest.getTarget()); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + final HttpCookie httpCookie = httpRequest.getCookie(); + String sessionId = httpCookie.getCookie("JSESSIONID"); + if (sessionId != null && SessionManager.getInstance().findSession(sessionId) != null) { // already login user + httpResponse.addHeader(LOCATION, "/index.html"); + httpResponse.setStatusCode(FOUND); + } else { // not login user + handleResource("/login.html", httpRequest, httpResponse); + } + } + + @Override + protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final HttpCookie httpCookie = httpRequest.getCookie(); + String sessionId = httpCookie.getCookie("JSESSIONID"); + + final Optional user = InMemoryUserRepository.findByAccount(httpRequest.getBody().get("account")); + if (user.isEmpty() || !user.get().checkPassword(httpRequest.getBody().get("password"))) { + // invalid user + httpResponse.addHeader(LOCATION, "/401.html"); + httpResponse.setStatusCode(FOUND); + return; + } + + if (sessionId != null) { // if already have session + httpResponse.addHeader(LOCATION, "/index.html"); + httpResponse.setStatusCode(FOUND); + return; + } + + // if no session + final Session session = new Session(String.valueOf(UUID.randomUUID())); + session.setAttribute("user", user); + SessionManager.getInstance().add(session); + sessionId = session.getId(); + + httpResponse.addHeader(LOCATION, "/index.html"); + httpResponse.addHeader(SET_COOKIE, "JSESSIONID=" + sessionId); + httpResponse.setStatusCode(FOUND); + } +} 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..ff57fca868 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,39 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.controller.HttpController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.util.Set; + +import static org.apache.coyote.http11.common.HttpHeaderType.LOCATION; +import static org.apache.coyote.http11.response.HttpStatusCode.FOUND; + +public class RegisterController extends HttpController { + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final Set requestType = Set.of("/register"); + return requestType.contains(httpRequest.getTarget()); + } + + @Override + public void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + handleResource("/register.html", httpRequest, httpResponse); + } + + @Override + public void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) { + final User newUser = new User( + httpRequest.getBody().get("account"), + httpRequest.getBody().get("password"), + httpRequest.getBody().get("email") + ); + InMemoryUserRepository.save(newUser); + httpResponse.addHeader(LOCATION, "/index.html"); + httpResponse.setStatusCode(FOUND); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RootController.java b/tomcat/src/main/java/nextstep/jwp/controller/RootController.java new file mode 100644 index 0000000000..f761b817bc --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RootController.java @@ -0,0 +1,27 @@ +package nextstep.jwp.controller; + +import org.apache.catalina.controller.HttpController; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.util.*; + +import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_TYPE; +import static org.apache.coyote.http11.common.MediaType.TEXT_HTML; +import static org.apache.coyote.http11.response.HttpStatusCode.OK; + +public class RootController extends HttpController { + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final Set requestType = Set.of("/"); + return requestType.contains(httpRequest.getTarget()); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) { + httpResponse.addHeader(CONTENT_TYPE, TEXT_HTML.stringifyWithUtf()); + httpResponse.setStatusCode(OK); + httpResponse.setBody("Hello world!"); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Controller.java b/tomcat/src/main/java/org/apache/catalina/Controller.java new file mode 100644 index 0000000000..4d2b6fcb43 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Controller.java @@ -0,0 +1,14 @@ +package org.apache.catalina; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; + +public interface Controller { + + void init(); + void destory(); + void service(HttpRequest request, HttpResponse response) throws IOException; + +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index 1153287219..85abe87dda 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,7 +1,8 @@ package org.apache.catalina; +import org.apache.catalina.session.Session; + import java.io.IOException; -import nextstep.jwp.model.Session; /** * A Manager manages the pool of Sessions that are associated with a diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..e6e05510d4 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 org.apache.coyote.RequestMapping; 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,19 +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_MAX_THREADS = 250; private final ServerSocket serverSocket; + private final ExecutorService executor; private boolean stopped; public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS); } - public Connector(final int port, final int acceptCount) { + public Connector(final int port, final int acceptCount, final int maxThreads) { this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; + this.executor = Executors.newFixedThreadPool(maxThreads); } + private ServerSocket createServerSocket(final int port, final int acceptCount) { try { final int checkedPort = checkPort(port); @@ -66,8 +73,8 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); - new Thread(processor).start(); + var processor = new Http11Processor(connection, new RequestMapping()); + executor.submit(processor); } public void stop() { diff --git a/tomcat/src/main/java/org/apache/catalina/controller/HttpController.java b/tomcat/src/main/java/org/apache/catalina/controller/HttpController.java new file mode 100644 index 0000000000..04bfa5afe3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/controller/HttpController.java @@ -0,0 +1,74 @@ +package org.apache.catalina.controller; + +import org.apache.catalina.Controller; +import org.apache.coyote.http11.HttpException; +import org.apache.coyote.http11.common.ResourceContentTypeResolver; +import org.apache.coyote.http11.common.ResourceReader; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.net.URL; + +import static org.apache.coyote.http11.common.HttpHeaderType.*; +import static org.apache.coyote.http11.response.HttpStatusCode.*; + +public abstract class HttpController implements Controller { + + @Override + public void init() { + } + + @Override + public void destory() { + } + + @Override + public void service(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + final String method = httpRequest.getMethod(); + try { + if ("GET".equals(method)) { + doGet(httpRequest, httpResponse); + } + if ("POST".equals(method)) { + doPost(httpRequest, httpResponse); + } + } + catch (final HttpException e) { + if (e.getStatusCode() == NOT_FOUND) { + httpResponse.setStatusCode(NOT_FOUND); + httpResponse.addHeader(LOCATION, "/404.html"); + } else { + httpResponse.setStatusCode(e.getStatusCode()); + httpResponse.setBody(e.getMessage()); + } + } + } + + protected void handleResource( + final String resourceName, + final HttpRequest httpRequest, + final HttpResponse httpResponse + ) throws IOException { + final URL resourceUrl = ResourceReader.getResourceUrl(resourceName); + if (resourceUrl == null) { + throw new HttpException(NOT_FOUND, "The resource corresponding to the request does not exist"); + } + + final String contentType = ResourceContentTypeResolver.getResourceContentType(httpRequest.getHeaders().getHeaderValue(ACCEPT), resourceName); + final String responseBody = ResourceReader.read(resourceUrl); + httpResponse.setStatusCode(OK); + httpResponse.addHeader(CONTENT_TYPE, contentType); + httpResponse.setBody(responseBody); + } + + public abstract boolean canHandle(final HttpRequest httpRequest); + + protected void doGet(final HttpRequest request, final HttpResponse response) throws IOException { + response.setStatusCode(METHOD_NOT_ALLOWED); + } + + protected void doPost(final HttpRequest request, final HttpResponse response) throws IOException { + response.setStatusCode(METHOD_NOT_ALLOWED); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/controller/ResourceController.java b/tomcat/src/main/java/org/apache/catalina/controller/ResourceController.java new file mode 100644 index 0000000000..44604452b1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/controller/ResourceController.java @@ -0,0 +1,19 @@ +package org.apache.catalina.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; + +public class ResourceController extends HttpController { + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + return "GET".equals(httpRequest.getMethod()); + } + + @Override + protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + handleResource(httpRequest.getTarget(), httpRequest, httpResponse); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/model/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java similarity index 91% rename from tomcat/src/main/java/nextstep/jwp/model/Session.java rename to tomcat/src/main/java/org/apache/catalina/session/Session.java index 2ac8ceb637..3ebeb662a4 100644 --- a/tomcat/src/main/java/nextstep/jwp/model/Session.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -1,4 +1,4 @@ -package nextstep.jwp.model; +package org.apache.catalina.session; import java.util.HashMap; import java.util.Map; diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java similarity index 50% rename from tomcat/src/main/java/org/apache/catalina/SessionManager.java rename to tomcat/src/main/java/org/apache/catalina/session/SessionManager.java index 6a5e32e6b1..530a07d9aa 100644 --- a/tomcat/src/main/java/org/apache/catalina/SessionManager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -1,12 +1,20 @@ -package org.apache.catalina; +package org.apache.catalina.session; + +import org.apache.catalina.Manager; -import java.util.HashMap; import java.util.Map; -import nextstep.jwp.model.Session; +import java.util.concurrent.ConcurrentHashMap; public class SessionManager implements Manager { - private static final Map SESSIONS = new HashMap<>(); + private static final Map SESSIONS = new ConcurrentHashMap<>(); + private static final SessionManager instance = new SessionManager(); + + private SessionManager() {} + + public static SessionManager getInstance() { + return instance; + } @Override public void add(final Session session) { @@ -22,6 +30,4 @@ public Session findSession(final String id) { public void remove(final Session session) { SESSIONS.remove(session.getId()); } - - public SessionManager() {} } diff --git a/tomcat/src/main/java/org/apache/coyote/RequestMapping.java b/tomcat/src/main/java/org/apache/coyote/RequestMapping.java new file mode 100644 index 0000000000..2f372b656a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/RequestMapping.java @@ -0,0 +1,33 @@ +package org.apache.coyote; + +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; +import nextstep.jwp.controller.RootController; +import org.apache.catalina.controller.HttpController; +import org.apache.catalina.controller.ResourceController; +import org.apache.coyote.http11.request.HttpRequest; + +import java.util.List; + +public class RequestMapping { + + private final List controllers; + + public RequestMapping() { + this.controllers = List.of( + new RootController(), + new LoginController(), + new RegisterController(), + new ResourceController() + ); + } + + public HttpController getController(final HttpRequest httpRequest) { + for (final HttpController controller : controllers) { + if (controller.canHandle(httpRequest)) { + return controller; + } + } + return null; + } +} 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 b45081fb6c..a04123580f 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,35 +1,29 @@ package org.apache.coyote.http11; -import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; -import nextstep.jwp.model.Session; -import nextstep.jwp.model.User; -import org.apache.catalina.SessionManager; +import org.apache.catalina.Controller; import org.apache.coyote.Processor; +import org.apache.coyote.RequestMapping; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestParser; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; -import java.net.URL; -import java.nio.file.Files; -import java.util.Optional; -import java.util.UUID; - -import static org.apache.coyote.http11.HttpHeaderType.*; -import static org.apache.coyote.http11.HttpStatusCode.*; 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; + private final RequestMapping requestMapping; - public Http11Processor(final Socket connection) { + public Http11Processor(final Socket connection, final RequestMapping requestMapping) { this.connection = connection; + this.requestMapping = requestMapping; } @Override @@ -46,138 +40,15 @@ public void process(final Socket connection) { final HttpRequestParser httpRequestParser = new HttpRequestParser(reader); final HttpRequest httpRequest = httpRequestParser.parse(); - final HttpCookie httpCookie = HttpCookie.of(httpRequest.getHeader().get("Cookie")); - // TODO: generate RequestHandler - final HttpResponse httpResponse = handleRequest(httpRequest); + final Controller controller = requestMapping.getController(httpRequest); + final HttpResponse httpResponse = HttpResponse.init(); + + controller.service(httpRequest, httpResponse); outputStream.write(httpResponse.stringify().getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } - - private HttpResponse handleRequest(final HttpRequest httpRequest) throws IOException { - if (httpRequest.getTarget().equals("/")) { - return handleRootRequest(); - } - - if (httpRequest.getTarget().equals("/login")) { - return handleLoginRequest(httpRequest); - } - - if (httpRequest.getTarget().equals("/register")) { - return handleRegisterRequest(httpRequest); - } - - return handleResourceRequest(httpRequest, httpRequest.getTarget()); - } - - private HttpResponse handleRootRequest() { - final HttpResponse httpResponse = HttpResponse.init(); - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.setStatusCode(OK); - httpResponse.setBody("Hello world!"); - return httpResponse; - } - - private HttpResponse handleRegisterRequest(final HttpRequest httpRequest) throws IOException { - if (httpRequest.getMethod().equals("GET")) { - return handleResourceRequest(httpRequest, "register.html"); - } - - if (httpRequest.getMethod().equals("POST")) { - final HttpResponse httpResponse = HttpResponse.init(); - final User newUser = new User( - httpRequest.getBody().get("account"), - httpRequest.getBody().get("password"), - httpRequest.getBody().get("email") - ); - InMemoryUserRepository.save(newUser); - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.addHeader(LOCATION, "/index.html"); - httpResponse.setStatusCode(FOUND); - return httpResponse; - } - return handleResourceRequest(httpRequest, "404.html"); // Method Not Allowed - } - - private HttpResponse handleLoginRequest(final HttpRequest httpRequest) throws IOException { - final HttpCookie httpCookie = HttpCookie.of(httpRequest.getHeader().get("Cookie")); - String sessionId = httpCookie.getCookie("JSESSIONID"); - - if (httpRequest.getMethod().equals("GET")) { - final HttpResponse httpResponse = HttpResponse.init(); - - final Session session = sessionManager.findSession(sessionId); - if (session != null) { - // already login user - httpResponse.setStatusCode(FOUND); - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.addHeader(LOCATION, "/index.html"); - return httpResponse; - } - // not login user - return handleResourceRequest(httpRequest, "login.html"); - } - - if (httpRequest.getMethod().equals("POST")) { - final HttpResponse httpResponse = HttpResponse.init(); - - Optional user = InMemoryUserRepository.findByAccount(httpRequest.getBody().get("account")); - if (user.isEmpty() || !user.get().checkPassword(httpRequest.getBody().get("password"))) { - // invalid user - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.addHeader(LOCATION, "/401.html"); - httpResponse.setStatusCode(FOUND); - return httpResponse; - } - - // valid user - log.info("user: {}", user.get()); - - if (sessionId != null) { // if already have session - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.addHeader(LOCATION, "/index.html"); - httpResponse.setStatusCode(FOUND); - return httpResponse; - } - - // if no session - Session session = new Session(String.valueOf(UUID.randomUUID())); - session.setAttribute("user", user); - sessionManager.add(session); - sessionId = session.getId(); - - httpResponse.addHeader(CONTENT_TYPE, "text/html;charset=utf-8"); - httpResponse.addHeader(LOCATION, "/index.html"); - httpResponse.addHeader(SET_COOKIE, "JSESSIONID=" + sessionId); - httpResponse.setStatusCode(FOUND); - return httpResponse; - } - return handleResourceRequest(httpRequest, "404.html"); // Method Not Allowed - } - - private HttpResponse handleResourceRequest(final HttpRequest httpRequest, final String resourceUrl) throws IOException { - final HttpResponse httpResponse = HttpResponse.init(); - String contentType = "text/html;charset=utf-8"; - - if (httpRequest.getHeader().get("Accept") != null) { - contentType = httpRequest.getHeader().get("Accept").split(",")[0]; - } - - URL resource = getClass().getClassLoader().getResource("static/" + resourceUrl); - if (resource != null) { - httpResponse.setStatusCode(OK); - } else { - resource = getClass().getClassLoader().getResource("static/" + "404.html"); - httpResponse.setStatusCode(NOT_FOUND); - contentType = "text/html;charset=utf-8"; - } - - final String responseBody = new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - httpResponse.addHeader(CONTENT_TYPE, contentType); - httpResponse.setBody(responseBody); - return httpResponse; - } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpException.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpException.java new file mode 100644 index 0000000000..bc5df2628e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpException.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11; + +import org.apache.coyote.http11.response.HttpStatusCode; + +public class HttpException extends RuntimeException { + + private final HttpStatusCode statusCode; + private final String errMsg; + + + public HttpException(final HttpStatusCode statusCode, final String errMsg) { + super(String.join("\n", + "title: " + statusCode.getMessage(), + "status: " + statusCode.getCode(), + "detail: " + errMsg)); + + this.statusCode = statusCode; + this.errMsg = errMsg; + } + + public HttpStatusCode getStatusCode() { + return statusCode; + } + + public String getErrMsg() { + return errMsg; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java deleted file mode 100644 index e5e7570c3f..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.HashMap; -import java.util.Map; - -import static org.apache.coyote.http11.HttpHeaderType.CONTENT_LENGTH; - -public class HttpHeaders { - - private final Map headers = new HashMap<>(); - - public String getContentLength() { - return headers.get(CONTENT_LENGTH.getName()); - } - - public String getHeaderValue(final HttpHeaderType type) { - return headers.get(type.getName()); - } - - public void setHeaderValue(final HttpHeaderType type, final String value) { - headers.put(type.getName(), value); - } - - public void setHeaderValue(final String type, final String value) { - headers.put(type, value); - } - - public Map getHeaders() { - return headers; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java deleted file mode 100644 index e0a3b573cd..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.Map; - -public class HttpRequest { - - private final RequestLine requestLine; - private final Map header; - private final Map body; - - private HttpRequest(final RequestLine requestLine, final Map header, final Map body) { - this.requestLine = requestLine; - this.header = header; - this.body = body; - } - - public static HttpRequest of(final RequestLine requestLine, final Map header, final Map body) { - return new HttpRequest(requestLine, header, body); - } - - public String getMethod() { - return requestLine.getMethod(); - } - - public String getTarget() { - return requestLine.getTarget(); - } - - public String getVersion() { - return requestLine.getVersion(); - } - - public Map getHeader() { - return header; - } - - public Map getBody() { - return body; - } -} 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 bdc19d6c67..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestParser.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.BufferedReader; -import java.io.IOException; -import java.net.URLDecoder; -import java.util.HashMap; -import java.util.Map; - -public class HttpRequestParser { - - private final BufferedReader reader; - - public HttpRequestParser(final BufferedReader reader) { - this.reader = reader; - } - - public HttpRequest parse() { - try { - final RequestLine requestLine = parseRequestLine(); - final Map requestHeader = parseRequestHeader(); - if (requestHeader.get("Content-Length") != null) { - final Map body = parseRequestBody(requestHeader.get("Content-Length")); - return HttpRequest.of(requestLine, requestHeader, body); - } - return HttpRequest.of(requestLine, requestHeader, new HashMap<>()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private RequestLine parseRequestLine() throws IOException { - final String line = reader.readLine(); - String[] s = line.split(" "); - return RequestLine.from(s[0], s[1], s[2]); - } - - private Map parseRequestHeader() throws IOException { - // TODO: Change to MultiValueMap - final Map header = new HashMap<>(); - String line; - while (!"".equals(line = reader.readLine())) { - String[] value = line.split(": "); - header.put(value[0], value[1]); - } - return header; - } - - private Map parseRequestBody(final String contentLengthHeader) throws IOException { - // TODO: Change to MultiValueMap - final Map body = new HashMap<>(); - int contentLength = Integer.parseInt(contentLengthHeader); - char[] buffer = new char[contentLength]; - reader.read(buffer, 0, contentLength); - - // TODO: Query Parse - for (String temp : new String(buffer).split("&")) { - String[] value = temp.split("="); - body.put(value[0], URLDecoder.decode(value[1], "UTF-8")); - } - return body; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java deleted file mode 100644 index a27f61c3b7..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/RequestLine.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.apache.coyote.http11; - -public class RequestLine { - - private final String method; - private final String target; - private final String version; - - private RequestLine(final String method, final String target, final String version) { - this.method = method; - this.target = target; - this.version = version; - } - - public static RequestLine from(final String method, final String target, final String version) { - return new RequestLine(method, target, version); - } - - public String getMethod() { - return method; - } - - public String getTarget() { - return target; - } - - public String getVersion() { - return version; - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java similarity index 85% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java rename to tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java index 6475ac5195..93eeb6a3ff 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java @@ -1,4 +1,4 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.common; import java.util.HashMap; import java.util.Map; @@ -15,6 +15,10 @@ public static HttpCookie of(final String cookies) { return new HttpCookie(parse(cookies)); } + public static HttpCookie empty() { + return new HttpCookie(new HashMap<>()); + } + private static Map parse(final String cookies) { if (cookies == null) { return new HashMap<>(); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaderType.java similarity index 84% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderType.java rename to tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaderType.java index ec017785a9..91d738674a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaderType.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaderType.java @@ -1,10 +1,11 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.common; public enum HttpHeaderType { CONTENT_TYPE("Content-Type"), CONTENT_LENGTH("Content-Length"), LOCATION("Location"), + COOKIE("Cookie"), SET_COOKIE("Set-Cookie"), ACCEPT("Accept"); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java new file mode 100644 index 0000000000..612534b490 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.common; + +import java.util.HashMap; +import java.util.Map; + +public class HttpHeaders { + + private final Map headers; + private HttpCookie cookie = HttpCookie.empty(); + + private HttpHeaders(final Map headers) { + this.headers = headers; + } + + public static HttpHeaders empty() { + return new HttpHeaders(new HashMap<>()); + } + + public static HttpHeaders of(final Map headers) { + return new HttpHeaders(headers); + } + + public String getHeaderValue(final HttpHeaderType type) { + return headers.get(type.getName()); + } + + public void setHeaderValue(final HttpHeaderType type, final String value) { + headers.put(type.getName(), value); + } + + public void setHeaderValue(final String type, final String value) { + headers.put(type, value); + } + + public HttpCookie getCookie() { + return cookie; + } + + public void setCookie(final HttpCookie httpCookie) { + this.cookie = httpCookie; + } + + public Map getHeaders() { + return headers; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/MediaType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/MediaType.java new file mode 100644 index 0000000000..a2be91287a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/MediaType.java @@ -0,0 +1,74 @@ +package org.apache.coyote.http11.common; + +import java.util.Arrays; + +public enum MediaType { + + ALL("*", "*"), + APPLICATION_FORM_URLENCODED("application", "x-www-form-urlencoded"), + APPLICATION_JSON("application", "json"), + APPLICATION_OCTET_STREAM("application", "octet-stream"), + APPLICATION_PDF("application", "pdf"), + APPLICATION_XML("application", "xml"), + IMAGE_GIF("image", "gif"), + IMAGE_JPEG("image", "jpeg"), + IMAGE_PNG("image", "png"), + MULTIPART_FORM_DATA("multipart", "form-data"), + MULTIPART_MIXED("multipart", "mixed"), + MULTIPART_RELATED("multipart", "related"), + TEXT_HTML("text", "html"), + TEXT_MARKDOWN("text", "markdown"), + TEXT_PLAIN("text", "plain"), + TEXT_XML("text", "xml"), + TEXT_JAVASCRIPT("text", "javascript"), + TEXT_CSS("text", "css"); + + private final String type; + private final String subType; + + MediaType(final String type, final String subType) { + this.type = type; + this.subType = subType; + } + + public static MediaType getMediaType(final String mediaType) { + return Arrays.stream(values()) + .filter(mediaType1 -> mediaType1.stringify().equals(mediaType)) + .findFirst() + .orElse(null); + } + + public static MediaType getMediaTypeByFileExtension(final String fileExtension) { + if ("js".equals(fileExtension)) { + return TEXT_JAVASCRIPT; + } + return Arrays.stream(values()) + .filter(mediaType -> mediaType.getSubType().equals(fileExtension)) + .findFirst() + .orElse(null); + } + + private String stringify() { + return type + "/" + subType; + } + + public static boolean isSupported(final MediaType mediaType) { + return mediaType != null && !mediaType.isWildCard(mediaType); + } + + private boolean isWildCard(final MediaType mediaType) { + return mediaType.equals(ALL); + } + + public String stringifyWithUtf() { + return type + "/" + subType + ";" + "charset=utf-8"; + } + + public String getType() { + return type; + } + + public String getSubType() { + return subType; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentTypeResolver.java b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentTypeResolver.java new file mode 100644 index 0000000000..d697940555 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentTypeResolver.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.common; + +import java.util.Optional; + +public class ResourceContentTypeResolver { + + public static String getResourceContentType(final String acceptValue, final String resourceName) { + return getFirstSupportedMediaType(acceptValue).orElse(getFileExtension(resourceName)); + } + + private static Optional getFirstSupportedMediaType(final String acceptValue) { + if (acceptValue != null) { + String[] mediaTypes = acceptValue.split(","); + for (String mediaTypeStr : mediaTypes) { + final MediaType mediaType = MediaType.getMediaType(mediaTypeStr.trim()); + if (MediaType.isSupported(mediaType)) { + return Optional.of(mediaType.stringifyWithUtf()); + } + } + } + return Optional.empty(); + } + + private static String getFileExtension(final String fileName) { + final int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex >= 0) { + final String fileExtension = fileName.substring(lastDotIndex + 1); + final MediaType mediaType = MediaType.getMediaTypeByFileExtension(fileExtension); + if (MediaType.isSupported(mediaType)) { + return mediaType.stringifyWithUtf(); + } + } + return MediaType.ALL.stringifyWithUtf(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceReader.java b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceReader.java new file mode 100644 index 0000000000..7776bb9a18 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceReader.java @@ -0,0 +1,20 @@ +package org.apache.coyote.http11.common; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class ResourceReader { + + private static final ClassLoader CLASS_LOADER = ClassLoader.getSystemClassLoader(); + + public static URL getResourceUrl(final String resourceName) { + return CLASS_LOADER.getResource("static" + resourceName); + } + + public static String read(final URL resource) throws IOException { + final byte[] bytes = Files.readAllBytes(new File(resource.getFile()).toPath()); + return new String(bytes); + } +} 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..6c4d3e7660 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,47 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpHeaders; + +import java.util.Map; + +public class HttpRequest { + + private final HttpRequestLine requestLine; + private final HttpHeaders headers; + private final Map body; + + private HttpRequest(final HttpRequestLine requestLine, final HttpHeaders headers, final Map body) { + this.requestLine = requestLine; + this.headers = headers; + this.body = body; + } + + public static HttpRequest from(final HttpRequestLine requestLine, final HttpHeaders header, final Map body) { + return new HttpRequest(requestLine, header, body); + } + + public String getMethod() { + return requestLine.getMethod(); + } + + public String getTarget() { + return requestLine.getTarget(); + } + + public String getVersion() { + return requestLine.getVersion(); + } + + public HttpHeaders getHeaders() { + return headers; + } + + public HttpCookie getCookie() { + return headers.getCookie(); + } + + public Map getBody() { + return body; + } +} 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..cc7b5ce7de --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class HttpRequestLine { + + private final String method; + private final String target; + private final Map queries; + private final String version; + + private HttpRequestLine(final String method, final String target, final Map queries, final String version) { + this.method = method; + this.target = target; + this.queries = queries; + this.version = version; + } + + public static HttpRequestLine from(final String method, final String target, final String version) { + return new HttpRequestLine(method, target, new HashMap<>(), version); + } + + public static HttpRequestLine from(final String method, final String target, final Map queries, final String version) { + return new HttpRequestLine(method, target, queries, version); + } + + public String getMethod() { + return method; + } + + public String getTarget() { + return target; + } + + public Map getQueries() { + return queries; + } + + public String getVersion() { + return version; + } +} 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..6e8bc169b7 --- /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 org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpHeaders; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_LENGTH; +import static org.apache.coyote.http11.common.HttpHeaderType.COOKIE; + +public class HttpRequestParser { + + private final BufferedReader reader; + + public HttpRequestParser(final BufferedReader reader) { + this.reader = reader; + } + + public HttpRequest parse() { + try { + final HttpRequestLine requestLine = parseRequestLine(); + final HttpHeaders requestHeaders = parseRequestHeader(); + final String contentLength = requestHeaders.getHeaderValue(CONTENT_LENGTH); + if (contentLength != null) { + final Map body = parseRequestBody(contentLength); + return HttpRequest.from(requestLine, requestHeaders, body); + } + return HttpRequest.from(requestLine, requestHeaders, new HashMap<>()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private HttpRequestLine parseRequestLine() throws IOException { + final String line = reader.readLine(); + final String[] s = line.split(" "); + if (s[1].contains("?")) { + String[] split = s[1].split("\\?"); + Map queryString = parseQueryString(split[1]); + return HttpRequestLine.from(s[0], split[0], queryString, s[2]); + } + return HttpRequestLine.from(s[0], s[1], s[2]); + } + + private Map parseQueryString(final String queryStrings) { + final Map queries = new HashMap<>(); + return parseUrlEncoded(queryStrings, queries); + } + + private HttpHeaders parseRequestHeader() throws IOException { + final Map headers = new HashMap<>(); + String line; + while (!"".equals(line = reader.readLine()) && line != null) { + String[] value = line.split(": "); + if (value.length >= 2) { + headers.put(value[0], value[1]); + } + } + final HttpHeaders httpHeaders = HttpHeaders.of(headers); + final HttpCookie httpCookie = HttpCookie.of(headers.get(COOKIE.getName())); + httpHeaders.setCookie(httpCookie); + return httpHeaders; + } + + private Map parseRequestBody(final String contentLengthHeader) throws IOException { + final Map body = new HashMap<>(); + int contentLength = Integer.parseInt(contentLengthHeader); + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + return parseUrlEncoded(new String(buffer), body); + } + + private Map parseUrlEncoded(final String queryStrings, final Map queries) { + for (String keyValuePair : queryStrings.split("&")) { + String[] keyValue = keyValuePair.split("="); + if (keyValue.length >= 2) { + queries.put(keyValue[0], URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8)); + } + } + return queries; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java similarity index 84% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java rename to tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java index ae1c03f091..fa4dc3c7fd 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -1,14 +1,16 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.response; + +import org.apache.coyote.http11.common.HttpHeaderType; +import org.apache.coyote.http11.common.HttpHeaders; import java.util.Map; -import static org.apache.coyote.http11.HttpStatusCode.NOT_FOUND; +import static org.apache.coyote.http11.response.HttpStatusCode.NOT_FOUND; public class HttpResponse { private HttpStatusCode statusCode; private final HttpHeaders headers; -// private final Map header; private String body; private HttpResponse(final HttpStatusCode statusCode, final HttpHeaders headers, final String body) { @@ -18,7 +20,7 @@ private HttpResponse(final HttpStatusCode statusCode, final HttpHeaders headers, } public static HttpResponse init() { - return new HttpResponse(NOT_FOUND, new HttpHeaders(), ""); + return new HttpResponse(NOT_FOUND, HttpHeaders.empty(), ""); } public void addHeader(final String key, final String value) { diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java similarity index 54% rename from tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java rename to tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java index 634bf44e1e..7f6a6fb15e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java @@ -1,10 +1,12 @@ -package org.apache.coyote.http11; +package org.apache.coyote.http11.response; public enum HttpStatusCode { OK(200, "OK"), FOUND(302, "Found"), - NOT_FOUND(404, "Not Found"); + BAD_REQUEST(400, "Bad Request"), + NOT_FOUND(404, "Not Found"), + METHOD_NOT_ALLOWED(405, "Method Not Allowed"); private final int code; private final String message; @@ -17,4 +19,12 @@ public enum HttpStatusCode { public String stringify() { return code + " " + message; } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } } 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..e62dd937c3 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/LoginControllerTest.java @@ -0,0 +1,240 @@ +package nextstep.jwp.controller; + +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatusCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.apache.coyote.http11.common.HttpHeaderType.*; +import static org.apache.coyote.http11.common.MediaType.TEXT_HTML; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class LoginControllerTest { + + private final LoginController loginController = new LoginController(); + private final HttpHeaders httpHeadersWithNoSession = HttpHeaders.empty(); + private final HttpHeaders httpHeadersWithSession = HttpHeaders.empty(); + + @BeforeEach + void setUp() { + String uuid = String.valueOf(UUID.randomUUID()); + httpHeadersWithSession.setCookie(HttpCookie.of("JSESSIONID=" + uuid)); + Session session = new Session(uuid); + SessionManager.getInstance().add(session); + } + + @DisplayName("LoginController는 정해진 request를 핸들링할 수 있다.") + @ParameterizedTest(name = "method: {0}, target: {1}") + @CsvSource({ + "POST, /login", + "GET, /login" + }) + void canHandle(String method, String target) { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from(method, target, "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = loginController.canHandle(httpRequest); + + // then + assertThat(actual).isTrue(); + } + + @DisplayName("LoginController는 정해진 request 외의 요청을 핸들링할 수 없다.") + @ParameterizedTest(name = "method: {0}, target: {1}") + @CsvSource({ + "POST, /register", + "GET, /" + }) + void canHandle_fail(String method, String target) { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from(method, target, "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = loginController.canHandle(httpRequest); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("세션이 없는 경우 GET 요청 시, login.html 리소스를 반환한다.") + @Test + void doGet_hasNoSession() throws IOException { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/login", "HTTP/1.1"), + httpHeadersWithNoSession, + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(CONTENT_TYPE.getName(), TEXT_HTML.stringifyWithUtf()); + + // when + loginController.doGet(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.OK); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("세션이 이미 있는 경우 GET 요청 시, index.html로 redirect 한다.") + @Test + void doGet_alreadyHasSession() throws IOException { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/login", "HTTP/1.1"), + httpHeadersWithSession, + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(LOCATION.getName(), "/index.html"); + + // when + loginController.doGet(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.FOUND); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("유효하지 않은 계정으로 POST 요청 시, 401.html로 redirect 한다.") + @Test + void doPost_invalidAccount() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("POST", "/login", "HTTP/1.1"), + httpHeadersWithNoSession, + Map.of( + "account", "invalid_user", + "password", "invalid_password" + ) + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(LOCATION.getName(), "/401.html"); + + // when + loginController.doPost(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.FOUND); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("세션이 이미 있는 경우 POST 요청 시, index.html로 redirect 한다.") + @Test + void doPost_alreadyHasSession() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("POST", "/login", "HTTP/1.1"), + httpHeadersWithSession, + Map.of( + "account", "gugu", + "password", "password" + ) + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(LOCATION.getName(), "/index.html"); + + // when + loginController.doPost(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.FOUND); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("세션이 없고, 유효한 유저의 POST 요청 시, set-cookie 설정 후 index.html로 redirect 한다.") + @Test + void doPost_hasNoSessionAndValidUser() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("POST", "/login", "HTTP/1.1"), + httpHeadersWithNoSession, + Map.of( + "account", "gugu", + "password", "password" + ) + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of( + SET_COOKIE.getName(), "JSESSIONID=", + LOCATION.getName(), "/index.html" + ); + + // when + loginController.doPost(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.FOUND); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .ignoringFields( "headers.Set-Cookie") + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } +} 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..9eceff2a4e --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RegisterControllerTest.java @@ -0,0 +1,144 @@ +package nextstep.jwp.controller; + +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.common.HttpCookie; +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatusCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.apache.coyote.http11.common.HttpHeaderType.*; +import static org.apache.coyote.http11.common.MediaType.TEXT_HTML; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RegisterControllerTest { + + private final RegisterController registerController = new RegisterController(); + private final HttpHeaders httpHeadersWithNoSession = HttpHeaders.empty(); + private final HttpHeaders httpHeadersWithSession = HttpHeaders.empty(); + + @BeforeEach + void setUp() { + String uuid = String.valueOf(UUID.randomUUID()); + httpHeadersWithSession.setCookie(HttpCookie.of("JSESSIONID=" + uuid)); + Session session = new Session(uuid); + SessionManager.getInstance().add(session); + } + + @DisplayName("RegisterController는 정해진 request를 핸들링할 수 있다.") + @ParameterizedTest(name = "method: {0}, target: {1}") + @CsvSource({ + "POST, /register", + "GET, /register" + }) + void canHandle(String method, String target) { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from(method, target, "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = registerController.canHandle(httpRequest); + + // then + assertThat(actual).isTrue(); + } + + @DisplayName("RegisterController는 정해진 request 외의 요청을 핸들링할 수 없다.") + @ParameterizedTest(name = "method: {0}, target: {1}") + @CsvSource({ + "POST, /login", + "GET, /" + }) + void canHandle_fail(String method, String target) { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from(method, target, "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = registerController.canHandle(httpRequest); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("GET 요청 시, register.html 리소스를 반환한다.") + @Test + void doGet_hasNoSession() throws IOException { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/register", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(CONTENT_TYPE.getName(), TEXT_HTML.stringifyWithUtf()); + + // when + registerController.doGet(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.OK); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("유효한 회원가입 정보로 POST 요청 시, 유저를 저장하고 index.html로 redirect 한다.") + @Test + void doPost() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("POST", "/login", "HTTP/1.1"), + httpHeadersWithNoSession, + Map.of( + "account", "new", + "password", "password", + "email", "new@gmail.com" + ) + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(LOCATION.getName(), "/index.html"); + + // when + registerController.doPost(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.FOUND); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } +} diff --git a/tomcat/src/test/java/nextstep/jwp/controller/RootControllerTest.java b/tomcat/src/test/java/nextstep/jwp/controller/RootControllerTest.java new file mode 100644 index 0000000000..8bfe083476 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RootControllerTest.java @@ -0,0 +1,93 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatusCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_TYPE; +import static org.apache.coyote.http11.common.MediaType.TEXT_HTML; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RootControllerTest { + + private final RootController rootController = new RootController(); + + @DisplayName("RootController는 정해진 request를 핸들링할 수 있다.") + @Test + void canHandle() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = rootController.canHandle(httpRequest); + + // then + assertThat(actual).isTrue(); + } + + @DisplayName("RootController는 정해진 request 외의 요청을 핸들링할 수 없다.") + @ParameterizedTest(name = "method: {0}, target: {1}") + @CsvSource({ + "POST, /register", + "GET, /login" + }) + void canHandle_fail(String method, String target) { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from(method, target, "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = rootController.canHandle(httpRequest); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("GET 요청 시, 해당되는 자원을 반환한다.") + @Test + void doGet() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(CONTENT_TYPE.getName(), TEXT_HTML.stringifyWithUtf()); + + // when + rootController.doGet(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.OK); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + softly.assertThat(httpResponse).extracting("body") + .isEqualTo("Hello world!"); + } + ); + } +} diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..cf474dea94 100644 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,5 +1,6 @@ package nextstep.org.apache.coyote.http11; +import org.apache.coyote.RequestMapping; import support.StubSocket; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; @@ -17,7 +18,7 @@ class Http11ProcessorTest { void process() { // given final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final var processor = new Http11Processor(socket, new RequestMapping()); // when processor.process(socket); @@ -44,7 +45,7 @@ void index() throws IOException { ""); final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); + final Http11Processor processor = new Http11Processor(socket, new RequestMapping()); // when processor.process(socket); diff --git a/tomcat/src/test/java/org/apache/catalina/controller/ResourceControllerTest.java b/tomcat/src/test/java/org/apache/catalina/controller/ResourceControllerTest.java new file mode 100644 index 0000000000..7219729a33 --- /dev/null +++ b/tomcat/src/test/java/org/apache/catalina/controller/ResourceControllerTest.java @@ -0,0 +1,134 @@ +package org.apache.catalina.controller; + +import org.apache.coyote.http11.HttpException; +import org.apache.coyote.http11.common.HttpHeaders; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestLine; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatusCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_TYPE; +import static org.apache.coyote.http11.common.MediaType.TEXT_HTML; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class ResourceControllerTest { + + private final ResourceController resourceController = new ResourceController(); + @DisplayName("ResourceController는 GET 메서드를 핸들링할 수 있다.") + @Test + void canHandle() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/index.html", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = resourceController.canHandle(httpRequest); + + // then + assertThat(actual).isTrue(); + } + + @DisplayName("ResourceController는 GET 이외의 메서드를 핸들링할 수 없다.") + @Test + void canHandle_fail() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("POST", "/index.html", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + // when + boolean actual = resourceController.canHandle(httpRequest); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("GET 요청 시, 정적 리소스를 반환한다.") + @Test + void doGet() throws IOException { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/index.html", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + Map headers = Map.of(CONTENT_TYPE.getName(), TEXT_HTML.stringifyWithUtf()); + + // when + resourceController.doGet(httpRequest, httpResponse); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.OK); + softly.assertThat(httpResponse).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + } + ); + } + + @DisplayName("존재하지 않는 리소스에 대한 GET 요청 시, HttpException이 발생한다.") + @Test + void doGet_invalidResource() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/invalid.html", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + + // when & then + assertSoftly( + softly -> { + softly.assertThatThrownBy(() -> resourceController.doGet(httpRequest, httpResponse)) + .isInstanceOf(HttpException.class); + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.NOT_FOUND); + } + ); + } + + @DisplayName("지원하지 않는 content-type의 resource에 대한 GET 요청 시, HttpException이 발생한다.") + @Test + void doGet_invalidContentType() { + // given + HttpRequest httpRequest = HttpRequest.from( + HttpRequestLine.from("GET", "/invalid.sql", "HTTP/1.1"), + HttpHeaders.empty(), + new HashMap<>() + ); + + HttpResponse httpResponse = HttpResponse.init(); + + // when & then + assertSoftly( + softly -> { + softly.assertThatThrownBy(() -> resourceController.doGet(httpRequest, httpResponse)) + .isInstanceOf(HttpException.class); + softly.assertThat(httpResponse).extracting("statusCode") + .usingRecursiveComparison() + .isEqualTo(HttpStatusCode.NOT_FOUND); + } + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/MediaTypeTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/MediaTypeTest.java new file mode 100644 index 0000000000..0e0ef7fd0d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/MediaTypeTest.java @@ -0,0 +1,73 @@ +package org.apache.coyote.http11.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class MediaTypeTest { + + @DisplayName("String 타입의 mediaType이 들어오면, 해당 mediaType에 맞는 MediaType 객체를 반환한다.") + @ParameterizedTest(name = "''{0}'' -> {1}") + @CsvSource({ + "text/html, TEXT_HTML", + "text/javascript, TEXT_JAVASCRIPT", + "text/css, TEXT_CSS", + "image/jpeg, IMAGE_JPEG" + }) + void getMediaType(String mediaType, MediaType expected) { + // given + + // when + MediaType actual = MediaType.getMediaType(mediaType); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("지원하지 않는 String 타입의 mediaType이 들어오면, null을 반환한다.") + @Test + void getMediaType_fail() { + // given + String invalidMediaType = "not/supported"; + + // when + MediaType actual = MediaType.getMediaType(invalidMediaType); + + // then + assertThat(actual).isNull(); + } + + @DisplayName("String 타입의 fileExtenstion이 들어오면, 해당 extension에 맞는 MediaType 객체를 반환한다.") + @ParameterizedTest(name = "''{0}'' -> {1}") + @CsvSource({ + "html, TEXT_HTML", + "js, TEXT_JAVASCRIPT", + "css, TEXT_CSS", + "jpeg, IMAGE_JPEG" + }) + void getMediaTypeByFileExtension(String fileExtension, MediaType expected) { + // given + + // when + MediaType actual = MediaType.getMediaTypeByFileExtension(fileExtension); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("지원하지 않는 String 타입의 fileExtenstion이 들어오면, null을 반환한다.") + @Test + void getMediaTypeByFileExtension_fail() { + // given + String invalidFileExtension = "invalid"; + + // when + MediaType actual = MediaType.getMediaTypeByFileExtension(invalidFileExtension); + + // then + assertThat(actual).isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceContentTypeResolverTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceContentTypeResolverTest.java new file mode 100644 index 0000000000..cd0e2200c0 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceContentTypeResolverTest.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResourceContentTypeResolverTest { + + @DisplayName("요청한 HttpHeader와 resource에 맞는 contentType을 반환한다.") + @ParameterizedTest(name = "accept: ''{1}'', resourceName: ''{2}'' -> {3}") + @CsvSource({ + "null, null, ALL", + "text/javascript, null, TEXT_JAVASCRIPT", + "null, styles.css, TEXT_CSS", + }) + void getContentType(String accept, String resourceName, MediaType expected) { + // given + + // when + String actual = ResourceContentTypeResolver.getResourceContentType(accept, resourceName); + + // then + assertThat(actual).isEqualTo(expected.stringifyWithUtf()); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceReaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceReaderTest.java new file mode 100644 index 0000000000..1d2056a8d7 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/common/ResourceReaderTest.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.common; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +class ResourceReaderTest { + + @DisplayName("resource 요청이 들어왔을 때, 해당 리소스의 위치를 URL 객체로 반환한다.") + @Test + void getResourceUrl() { + // given + String resourceName = "/index.html"; + + // when + URL actual = ResourceReader.getResourceUrl(resourceName); + + // then + assertThat(actual.getFile()).endsWith(resourceName); + } + + @DisplayName("존재하지 않는 resource 요청이 들어왔을 때, null 을 반환한다.") + @Test + void getResourceUrl_fail() { + // given + String resourceName = "/invalid.html"; + + // when + URL actual = ResourceReader.getResourceUrl(resourceName); + + // then + assertThat(actual).isNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java new file mode 100644 index 0000000000..8e9e71194d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestParserTest.java @@ -0,0 +1,153 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.common.HttpHeaders; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class HttpRequestParserTest { + + @DisplayName("request string을 HttpRequest 객체로 파싱한다.") + @Test + void parse() { + // given + String request = String.join("\r\n", + "GET /index.html HTTP/1.1", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding: gzip, deflate, br", + "Accept-Language: en-US,en;q=0.9", + "Connection: keep-alive", + "Host: localhost:8080", + "Referer: http://localhost:8080/index.html", + ""); + + Map headers = new HashMap<>(); + headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"); + headers.put("Accept-Encoding", "gzip, deflate, br"); + headers.put("Accept-Language", "en-US,en;q=0.9"); + headers.put("Connection", "keep-alive"); + headers.put("Host", "localhost:8080"); + headers.put("Referer", "http://localhost:8080/index.html"); + + // when + BufferedReader reader = new BufferedReader(new StringReader(request)); + HttpRequestParser parser = new HttpRequestParser(reader); + HttpRequest httpRequest = parser.parse(); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpRequest).extracting("requestLine") + .usingRecursiveComparison() + .isEqualTo(HttpRequestLine.from("GET", "/index.html", "HTTP/1.1")); + softly.assertThat(httpRequest).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + softly.assertThat(httpRequest).extracting("body") + .usingRecursiveComparison() + .isEqualTo(new HashMap<>()); + } + ); + } + + @DisplayName("(queryString) request string을 HttpRequest 객체로 파싱한다.") + @Test + void parse_queryString() { + // given + String request = String.join("\r\n", + "GET /?key1=1&key2=2 HTTP/1.1", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding: gzip, deflate, br", + "Accept-Language: en-US,en;q=0.9", + "Connection: keep-alive", + "Host: localhost:8080", + ""); + + Map query = new HashMap<>(); + query.put("key1", "1"); + query.put("key2", "2"); + + Map headers = new HashMap<>(); + headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"); + headers.put("Accept-Encoding", "gzip, deflate, br"); + headers.put("Accept-Language", "en-US,en;q=0.9"); + headers.put("Connection", "keep-alive"); + headers.put("Host", "localhost:8080"); + + // when + BufferedReader reader = new BufferedReader(new StringReader(request)); + HttpRequestParser parser = new HttpRequestParser(reader); + HttpRequest httpRequest = parser.parse(); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpRequest).extracting("requestLine") + .usingRecursiveComparison() + .isEqualTo(HttpRequestLine.from("GET", "/", query,"HTTP/1.1")); + softly.assertThat(httpRequest).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + softly.assertThat(httpRequest).extracting("body") + .usingRecursiveComparison() + .isEqualTo(new HashMap<>()); + } + ); + } + + @DisplayName("(body) request string을 HttpRequest 객체로 파싱한다.") + @Test + void parse_body() { + // given + String request = String.join("\r\n", + "POST /login HTTP/1.1", + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding: gzip, deflate, br", + "Accept-Language: en-US,en;q=0.9", + "Connection: keep-alive", + "Host: localhost:8080", + "Content-Length: 30", + "Content-Type: application/x-www-form-urlencoded", + "", + "account=gugu&password=password"); + + Map headers = new HashMap<>(); + headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"); + headers.put("Accept-Encoding", "gzip, deflate, br"); + headers.put("Accept-Language", "en-US,en;q=0.9"); + headers.put("Connection", "keep-alive"); + headers.put("Host", "localhost:8080"); + headers.put("Content-Length", "30"); + headers.put("Content-Type", "application/x-www-form-urlencoded"); + + Map body = new HashMap<>(); + body.put("account", "gugu"); + body.put("password", "password"); + + // when + BufferedReader reader = new BufferedReader(new StringReader(request)); + HttpRequestParser parser = new HttpRequestParser(reader); + HttpRequest httpRequest = parser.parse(); + + // then + assertSoftly( + softly -> { + softly.assertThat(httpRequest).extracting("requestLine") + .usingRecursiveComparison() + .isEqualTo(HttpRequestLine.from("POST", "/login","HTTP/1.1")); + softly.assertThat(httpRequest).extracting("headers") + .usingRecursiveComparison() + .isEqualTo(HttpHeaders.of(headers)); + softly.assertThat(httpRequest).extracting("body") + .usingRecursiveComparison() + .isEqualTo(body); + } + ); + } +}