diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..f63452964a 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,10 +19,11 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.assertj:assertj-core:3.26.0' 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..0141321ed1 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,38 @@ package cache.com.example.cachecontrol; +import java.time.Duration; + 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) { + registry.addInterceptor(getNoCacheInterceptor()); + registry.addInterceptor(getResourceCacheInterceptor()); + } + + // add no-cache, private cache + private WebContentInterceptor getNoCacheInterceptor() { + CacheControl cacheControl = CacheControl.noCache().cachePrivate(); + + return createWebContentInterceptorByCache(cacheControl, "/"); + } + + private WebContentInterceptor getResourceCacheInterceptor() { + CacheControl cacheControl = CacheControl.maxAge(Duration.ofDays(365)).cachePublic(); + + return createWebContentInterceptorByCache(cacheControl, "/resources/**"); + } + + private WebContentInterceptor createWebContentInterceptorByCache(CacheControl cacheControl, String path) { + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.addCacheMapping(cacheControl, path); + return 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..ab48ff798e 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() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new ShallowEtagHeaderFilter()); + registrationBean.addUrlPatterns("/etag", "/resources/*"); + return registrationBean; + } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..8b74bdfd88 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,8 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 + compression: + enabled: true + min-response-size: 10 diff --git a/tomcat/src/main/java/com/techcourse/Application.java b/tomcat/src/main/java/com/techcourse/Application.java index 6dc0e04e1d..b862334c52 100644 --- a/tomcat/src/main/java/com/techcourse/Application.java +++ b/tomcat/src/main/java/com/techcourse/Application.java @@ -4,8 +4,8 @@ public class Application { - public static void main(String[] args) { - final var tomcat = new Tomcat(); - tomcat.start(); - } + public static void main(String[] args) { + final var tomcat = new Tomcat(); + tomcat.start(); + } } diff --git a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java index d3fa57feeb..f8458a229f 100644 --- a/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/com/techcourse/db/InMemoryUserRepository.java @@ -1,27 +1,39 @@ package com.techcourse.db; -import com.techcourse.model.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -public class InMemoryUserRepository { - - private static final Map database = new ConcurrentHashMap<>(); +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); - database.put(user.getAccount(), user); - } - - public static void save(User user) { - database.put(user.getAccount(), user); - } +import com.techcourse.model.User; - public static Optional findByAccount(String account) { - return Optional.ofNullable(database.get(account)); - } +public class InMemoryUserRepository { - private InMemoryUserRepository() {} + private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + private static final Map database = new ConcurrentHashMap<>(); + + static { + final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); + database.put(user.getAccount(), user); + } + + public static void save(User user) { + if (database.containsKey(user.getAccount())) { + throw new IllegalArgumentException("account is already exist"); + } + database.put(user.getAccount(), user); + } + + public static Optional findByAccount(String account) { + if (account == null) { + log.info("account not exist"); + return Optional.empty(); + } + return Optional.ofNullable(database.get(account)); + } + + private InMemoryUserRepository() { + } } diff --git a/tomcat/src/main/java/com/techcourse/exception/UncheckedServletException.java b/tomcat/src/main/java/com/techcourse/exception/UncheckedServletException.java index 64466b42de..d89ead5f52 100644 --- a/tomcat/src/main/java/com/techcourse/exception/UncheckedServletException.java +++ b/tomcat/src/main/java/com/techcourse/exception/UncheckedServletException.java @@ -2,7 +2,7 @@ public class UncheckedServletException extends RuntimeException { - public UncheckedServletException(Exception e) { - super(e); - } + public UncheckedServletException(Exception e) { + super(e); + } } diff --git a/tomcat/src/main/java/com/techcourse/model/User.java b/tomcat/src/main/java/com/techcourse/model/User.java index e8cf4c8e68..83dc59dc0a 100644 --- a/tomcat/src/main/java/com/techcourse/model/User.java +++ b/tomcat/src/main/java/com/techcourse/model/User.java @@ -1,38 +1,59 @@ package com.techcourse.model; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class User { - private final Long id; - private final String account; - private final String password; - private final String email; - - public User(Long id, String account, String password, String email) { - this.id = id; - this.account = account; - this.password = password; - this.email = email; - } - - public User(String account, String password, String email) { - this(null, account, password, email); - } - - public boolean checkPassword(String password) { - return this.password.equals(password); - } - - public String getAccount() { - return account; - } - - @Override - public String toString() { - return "User{" + - "id=" + id + - ", account='" + account + '\'' + - ", email='" + email + '\'' + - ", password='" + password + '\'' + - '}'; - } + private static final Logger log = LoggerFactory.getLogger(User.class); + private final Long id; + private final String account; + private final String password; + private final String email; + + public User(Long id, String account, String password, String email) { + validateIsNotEmptyValue(account, password, email); + this.id = id; + this.account = account; + this.password = password; + this.email = email; + } + + private void validateIsNotEmptyValue(String account, String password, String email) { + if (account == null || account.isBlank()) { + throw new IllegalArgumentException("Account is empty"); + } + if (password == null || password.isBlank()) { + throw new IllegalArgumentException("Password is empty"); + } + if (email == null || email.isBlank()) { + throw new IllegalArgumentException("Email is empty"); + } + } + + public User(String account, String password, String email) { + this(null, account, password, email); + } + + public boolean checkPassword(String password) { + if (password == null) { + log.info("password not exist"); + return false; + } + return this.password.equals(password); + } + + public String getAccount() { + return account; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; + } } diff --git a/tomcat/src/main/java/com/techcourse/web/HttpResponse.java b/tomcat/src/main/java/com/techcourse/web/HttpResponse.java deleted file mode 100644 index 53665acdcb..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/HttpResponse.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.techcourse.web; - -import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; -import java.util.Map; - -public class HttpResponse { - - private final String protocol; - private final HttpStatusCode statusCode; - private final Map headers; - private final HttpResponse.Body body; - - private HttpResponse(String protocol, HttpStatusCode statusCode, Map headers, - HttpResponse.Body body) { - this.protocol = protocol; - this.statusCode = statusCode; - this.headers = headers; - this.body = body; - } - - public static HttpResponse.Builder builder() { - return new HttpResponse.Builder(); - } - - public String createResponseMessage() { - StringBuilder response = new StringBuilder(); - - // start line - response.append(protocol) - .append(" ") - .append(statusCode.getCode()) - .append(" ") - .append(statusCode.getMessage()) - .append(" ") - .append("\r\n"); - - // header - headers.forEach((key, value) -> response.append(key).append(": ").append(value).append(" ").append("\r\n")); - - // crlf - response.append("\r\n"); - - // body - if (body != null) { - response.append(body.getContent()); - } - - return response.toString(); - } - - public String getProtocol() { - return protocol; - } - - public HttpStatusCode getStatusCode() { - return statusCode; - } - - public Map getHeaders() { - return headers; - } - - public Body getBody() { - return body; - } - - @Override - public String toString() { - return "HttpResponse{" + - "protocol='" + protocol + '\'' + - ", statusCode=" + statusCode + - ", headers=" + headers + - ", body=" + body + - '}'; - } - - public static class Body { - - private static final String DEFAULT_CONTENT_CHARSET = ";charset=utf-8"; - - private final String contentType; - private final int contentLength; - private final String content; - - public static HttpResponse.Body fromPath(String path) throws IOException { - String fileName = path.contains(".") ? path : path + ".html"; - URL resource = HttpResponse.Body.class.getResource("/static" + fileName); - if (resource == null) { - throw new IllegalArgumentException("resource not found: " + "/static" + fileName); - } - - Path resourcePath = Paths.get(resource.getPath()); - - String contentType = Files.probeContentType(resourcePath) + DEFAULT_CONTENT_CHARSET; - String body = Files.readString(resourcePath); - int contentLength = body.getBytes(StandardCharsets.UTF_8).length; - - return new HttpResponse.Body(contentType, contentLength, body); - } - - public static HttpResponse.Body fromString(String body) { - String contentType = "text/html" + DEFAULT_CONTENT_CHARSET; - int contentLength = body.getBytes(StandardCharsets.UTF_8).length; - - return new HttpResponse.Body(contentType, contentLength, body); - } - - private Body(String contentType, int contentLength, String content) { - this.contentType = contentType; - this.contentLength = contentLength; - this.content = content; - } - - public String getContentType() { - return contentType; - } - - public int getContentLength() { - return contentLength; - } - - public String getContent() { - return content; - } - - @Override - public String toString() { - return "Body{" + - "contentType='" + contentType + '\'' + - ", contentLength=" + contentLength + - ", content='" + content + '\'' + - '}'; - } - } - - public static class Builder { - - private String protocol; - private HttpStatusCode statusCode; - private Map headers; - private HttpResponse.Body body; - - public Builder() { - headers = new LinkedHashMap<>(); - } - - public Builder protocol(String protocol) { - this.protocol = protocol; - return this; - } - - public Builder statusCode(HttpStatusCode statusCode) { - this.statusCode = statusCode; - return this; - } - - public Builder header(String key, String value) { - headers.put(key, value); - return this; - } - - public Builder body(HttpResponse.Body body) { - this.body = body; - headers.put("Content-Type", body.getContentType()); - headers.put("Content-Length", String.valueOf(body.getContentLength())); - return this; - } - - public HttpResponse build() { - return new HttpResponse(protocol, statusCode, headers, body); - } - } -} diff --git a/tomcat/src/main/java/com/techcourse/web/annotation/Param.java b/tomcat/src/main/java/com/techcourse/web/annotation/Param.java deleted file mode 100644 index 41e7ec2915..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/annotation/Param.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.techcourse.web.annotation; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Param { - - String key(); - - boolean required(); -} diff --git a/tomcat/src/main/java/com/techcourse/web/annotation/Query.java b/tomcat/src/main/java/com/techcourse/web/annotation/Query.java deleted file mode 100644 index 9bcefa4ca6..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/annotation/Query.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.techcourse.web.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Query { - - Param[] params(); -} diff --git a/tomcat/src/main/java/com/techcourse/web/annotation/Request.java b/tomcat/src/main/java/com/techcourse/web/annotation/Request.java deleted file mode 100644 index 92e57e4f77..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/annotation/Request.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.techcourse.web.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Request { - - String path(); - - String method(); -} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/Handler.java b/tomcat/src/main/java/com/techcourse/web/handler/Handler.java new file mode 100644 index 0000000000..f46a88ff9e --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/Handler.java @@ -0,0 +1,29 @@ +package com.techcourse.web.handler; + +import java.io.IOException; + +import org.apache.coyote.http11.http.HttpHeader; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; + +import com.techcourse.web.util.ResourceLoader; + +public interface Handler { + + boolean isSupport(HttpRequestLine requestLine); + + HttpResponse handle(HttpRequest request) throws IOException; + + default HttpResponse notFoundResponse() throws IOException { + HttpResponseBody notFoundPage = ResourceLoader.getInstance().loadResource("/404.html"); + return HttpResponse.notFound(new HttpResponseHeader(), notFoundPage); + } + + default HttpResponse redirect(HttpResponseHeader header, String location) { + header.addHeader(HttpHeader.LOCATION.getName(), location); + return HttpResponse.redirect(header); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/HandlerMapper.java b/tomcat/src/main/java/com/techcourse/web/handler/HandlerMapper.java new file mode 100644 index 0000000000..9100053566 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/HandlerMapper.java @@ -0,0 +1,30 @@ +package com.techcourse.web.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseHeader; + +public class HandlerMapper { + + private static final List handlers = new ArrayList<>(); + + static { + handlers.add(RootPageHandler.getInstance()); + handlers.add(ResourceHandler.getInstance()); + handlers.add(LoginHandler.getInstance()); + handlers.add(RegisterHandler.getInstance()); + } + + public static Handler findHandler(HttpRequest httpRequest) { + HttpRequestLine requestLine = httpRequest.getRequestLine(); + return handlers.stream() + .filter(h -> h.isSupport(requestLine)) + .findFirst() + .orElse(NotFoundHandler.getInstance()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/LoginHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/LoginHandler.java index dc67428115..d7fc22272c 100644 --- a/tomcat/src/main/java/com/techcourse/web/handler/LoginHandler.java +++ b/tomcat/src/main/java/com/techcourse/web/handler/LoginHandler.java @@ -2,24 +2,32 @@ import java.io.IOException; import java.util.Map; -import java.util.Optional; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.http.HttpCookie; +import org.apache.coyote.http11.http.request.HttpMethod; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestHeader; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.request.HttpRequestUrl; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; +import org.apache.coyote.http11.http.session.SessionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.techcourse.db.InMemoryUserRepository; import com.techcourse.model.User; -import com.techcourse.web.HttpResponse; -import com.techcourse.web.HttpStatusCode; -import com.techcourse.web.annotation.Param; -import com.techcourse.web.annotation.Query; -import com.techcourse.web.annotation.Request; +import com.techcourse.web.util.FormUrlEncodedParser; +import com.techcourse.web.util.JsessionIdGenerator; +import com.techcourse.web.util.LoginChecker; +import com.techcourse.web.util.ResourceLoader; -public class LoginHandler extends RequestHandler { +public class LoginHandler implements Handler { private static final Logger log = LoggerFactory.getLogger(LoginHandler.class); private static final LoginHandler instance = new LoginHandler(); + private static final String LOGIN_PATH = "/login"; private LoginHandler() { } @@ -28,38 +36,67 @@ public static LoginHandler getInstance() { return instance; } - @Request(path = "/login", method = "GET") - @Query(params = { - @Param(key = "account", required = true), - @Param(key = "password", required = true) - }) - public HttpResponse loginWithQuery(HttpRequest request) throws IOException { - Map query = request.getRequestQuery(); - String account = query.get("account"); - String password = query.get("password"); - - logUser(account, password); - - return HttpResponse.builder() - .protocol(request.getProtocol()) - .statusCode(HttpStatusCode.OK) - .body(HttpResponse.Body.fromPath(request.getRequestPath())) - .build(); + @Override + public boolean isSupport(HttpRequestLine requestLine) { + return requestLine.getRequestPath().startsWith(LOGIN_PATH); } - private void logUser(String account, String password) { - Optional userOptional = InMemoryUserRepository.findByAccount(account); - if (userOptional.isEmpty()) { - log.info("user not found. account: {}", account); - return; + @Override + public HttpResponse handle(HttpRequest request) throws IOException { + HttpRequestLine requestLine = request.getRequestLine(); + HttpRequestUrl requestUrl = request.getRequestLine().getHttpRequestUrl(); + HttpMethod method = requestLine.getMethod(); + + if (method == HttpMethod.GET && LOGIN_PATH.equals(requestUrl.getRequestUrl())) { + return loadLoginPage(request); + } + if (method == HttpMethod.POST) { + return login(request); + } + + return notFoundResponse(); + } + + private HttpResponse loadLoginPage(HttpRequest request) throws IOException { + if (LoginChecker.isLoggedIn(request)) { + return redirect(new HttpResponseHeader(), "/index.html"); } + HttpResponseBody body = ResourceLoader.getInstance().loadResource("/login.html"); + return HttpResponse.ok(new HttpResponseHeader(), body); + } + + private HttpResponse login(HttpRequest request) { + Map body = FormUrlEncodedParser.parse(request.getRequestBody()); + String account = body.get("account"); + String password = body.get("password"); - User user = userOptional.get(); - if (!user.checkPassword(password)) { - log.info("password not matched. account: {}", account); - return; + User user = InMemoryUserRepository.findByAccount(account).orElse(null); + if (isUserNotExist(user, password)) { + return redirect(new HttpResponseHeader(), "/401.html"); } - log.info("user: {}", user); + HttpResponseHeader responseHeader = createResponseHeader(request, user); + return redirect(responseHeader, "/index.html"); + } + + private HttpResponseHeader createResponseHeader(HttpRequest request, User user) { + HttpResponseHeader header = new HttpResponseHeader(); + String sessionId = JsessionIdGenerator.generate(); + header.addJSessionId(sessionId); + SessionManager.createSession(sessionId, user); + + return header; + } + + private boolean isNotExistJsessionid(HttpCookie httpCookie) { + return httpCookie == null || !httpCookie.hasJsessionId(); + } + + private boolean isUserNotExist(User user, String password) { + if (user == null) { + log.info("user is not exist"); + return true; + } + return !user.checkPassword(password); } } diff --git a/tomcat/src/main/java/com/techcourse/web/handler/NotFoundHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/NotFoundHandler.java new file mode 100644 index 0000000000..6fd89d8074 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/NotFoundHandler.java @@ -0,0 +1,34 @@ +package com.techcourse.web.handler; + +import java.io.IOException; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; + +import com.techcourse.web.util.ResourceLoader; + +public class NotFoundHandler implements Handler { + + private static final NotFoundHandler instance = new NotFoundHandler(); + + private NotFoundHandler() { + } + + public static NotFoundHandler getInstance() { + return instance; + } + + @Override + public boolean isSupport(HttpRequestLine requestLine) { + return true; + } + + @Override + public HttpResponse handle(HttpRequest request) throws IOException { + HttpResponseBody notFoundPage = ResourceLoader.getInstance().loadResource("/404.html"); + return HttpResponse.notFound(new HttpResponseHeader(), notFoundPage); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/PageHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/PageHandler.java deleted file mode 100644 index 6244cff645..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/handler/PageHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.techcourse.web.handler; - -import java.io.IOException; - -import org.apache.coyote.http11.HttpRequest; - -import com.techcourse.web.HttpResponse; -import com.techcourse.web.HttpStatusCode; -import com.techcourse.web.annotation.Request; - -public class PageHandler extends RequestHandler { - - private static final PageHandler instance = new PageHandler(); - - private PageHandler() { - } - - public static PageHandler getInstance() { - return instance; - } - - @Request(path = "*", method = "GET") - public HttpResponse getResource(HttpRequest request) throws IOException { - if (request.getRequestPath().contains("favicon")) { - return HttpResponse.builder() - .protocol(request.getProtocol()) - .statusCode(HttpStatusCode.NOT_FOUND) - .build(); - } - return HttpResponse.builder() - .protocol(request.getProtocol()) - .statusCode(HttpStatusCode.OK) - .body(getResponseBody(request.getRequestPath())) - .build(); - } - - private HttpResponse.Body getResponseBody(String requestTarget) throws IOException { - if (requestTarget.equals("/")) { - return HttpResponse.Body.fromString("Hello world!"); - } - return HttpResponse.Body.fromPath(requestTarget); - } -} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/RegisterHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/RegisterHandler.java new file mode 100644 index 0000000000..ad7fa2d2a4 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/RegisterHandler.java @@ -0,0 +1,70 @@ +package com.techcourse.web.handler; + +import java.io.IOException; +import java.util.Map; + +import org.apache.coyote.http11.http.request.HttpMethod; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import com.techcourse.web.util.FormUrlEncodedParser; +import com.techcourse.web.util.ResourceLoader; + +public class RegisterHandler implements Handler { + + private static final Logger log = LoggerFactory.getLogger(RegisterHandler.class); + private static final String REGISTER_PATH = "/register"; + private static final RegisterHandler instance = new RegisterHandler(); + + private RegisterHandler() { + } + + public static RegisterHandler getInstance() { + return instance; + } + + @Override + public boolean isSupport(HttpRequestLine requestLine) { + return requestLine.getRequestPath().startsWith(REGISTER_PATH); + } + + @Override + public HttpResponse handle(HttpRequest request) throws IOException { + HttpRequestLine requestLine = request.getRequestLine(); + HttpMethod method = requestLine.getMethod(); + + if (method == HttpMethod.GET) { + return loadRegisterPage(); + } + if (method == HttpMethod.POST) { + return register(request); + } + + return notFoundResponse(); + } + + private HttpResponse register(HttpRequest request) { + try { + Map data = FormUrlEncodedParser.parse(request.getRequestBody()); + User user = new User(data.get("account"), data.get("password"), data.get("email")); + InMemoryUserRepository.save(user); + + return redirect(new HttpResponseHeader(), "/index.html"); + } catch (IllegalArgumentException e) { + log.error("Failed to register user. {}", e.getMessage()); + return HttpResponse.badRequest(new HttpResponseHeader()); + } + } + + private HttpResponse loadRegisterPage() throws IOException { + HttpResponseBody body = ResourceLoader.getInstance().loadResource("/register.html"); + return HttpResponse.ok(new HttpResponseHeader(), body); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/RequestHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/RequestHandler.java deleted file mode 100644 index c6ba4384e8..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/handler/RequestHandler.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.techcourse.web.handler; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; - -import org.apache.coyote.http11.HttpRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.techcourse.web.HttpResponse; -import com.techcourse.web.HttpStatusCode; -import com.techcourse.web.annotation.Query; -import com.techcourse.web.annotation.Request; - -public abstract class RequestHandler { - - private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); - - public HttpResponse handle(HttpRequest request) { - Method handlerMethod = Arrays.stream(getClass().getDeclaredMethods()) - .filter(method -> method.isAnnotationPresent(Request.class)) - .filter(method -> { - Request requestAnnotation = method.getAnnotation(Request.class); - boolean isSameMethod = requestAnnotation.method().equals(request.getMethod()); - - String path = requestAnnotation.path(); - boolean isSamePath = path.equals("*") || path.equals(request.getRequestPath()); - - return isSameMethod && isSamePath; - }) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException( - "handler method not found: " + request.getMethod() + " " + request.getRequestPath())); - - if (handlerMethod.isAnnotationPresent(Query.class) && hasNotRequiredQueryParams(handlerMethod, request)) { - return HttpResponse.builder() - .protocol(request.getProtocol()) - .statusCode(HttpStatusCode.BAD_REQUEST) - .build(); - } - - try { - handlerMethod.setAccessible(true); - return (HttpResponse)handlerMethod.invoke(this, request); - } catch (IllegalAccessException | InvocationTargetException e) { - log.error(e.getMessage(), e); - return HttpResponse.builder() - .protocol(request.getProtocol()) - .statusCode(HttpStatusCode.INTERNAL_SERVER_ERROR) - .build(); - } - } - - private boolean hasNotRequiredQueryParams(Method handlerMethod, HttpRequest request) { - Query annotation = handlerMethod.getAnnotation(Query.class); - return Arrays.stream(annotation.params()) - .anyMatch(param -> param.required() && !request.getRequestQuery().containsKey(param.key())); - } -} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/RequestHandlerMapper.java b/tomcat/src/main/java/com/techcourse/web/handler/RequestHandlerMapper.java deleted file mode 100644 index bda9b829e8..0000000000 --- a/tomcat/src/main/java/com/techcourse/web/handler/RequestHandlerMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.techcourse.web.handler; - -import java.util.HashMap; -import java.util.Map; - -public class RequestHandlerMapper { - - private static final RequestHandlerMapper instance = new RequestHandlerMapper(); - private static final Map handlerMap = new HashMap<>(); - - static { - handlerMap.put("/", PageHandler.getInstance()); - handlerMap.put("^.*\\.(html|css|js|ico)$", PageHandler.getInstance()); - handlerMap.put("^(\\/login)(?!\\.).*", LoginHandler.getInstance()); - } - - private RequestHandlerMapper() { - } - - public static RequestHandlerMapper getInstance() { - return instance; - } - - public RequestHandler findHandler(String requestPath) { - return handlerMap.entrySet().stream() - .filter(entry -> requestPath.matches(entry.getKey())) - .map(Map.Entry::getValue) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("handler not found: " + requestPath)); - } -} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/ResourceHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/ResourceHandler.java new file mode 100644 index 0000000000..a2b6b3ef8c --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/ResourceHandler.java @@ -0,0 +1,44 @@ +package com.techcourse.web.handler; + +import java.io.IOException; +import java.util.regex.Pattern; + +import org.apache.coyote.http11.http.request.HttpMethod; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; + +import com.techcourse.web.util.ResourceLoader; + +public class ResourceHandler implements Handler { + + private static final ResourceHandler instance = new ResourceHandler(); + private static final String STATIC_FILE_EXTENSION_REGEX = ".*\\.(html|css|js|ico|svg)"; + private static final Pattern STATIC_FILE_PATTERN = Pattern.compile(STATIC_FILE_EXTENSION_REGEX); + + private ResourceHandler() { + } + + public static ResourceHandler getInstance() { + return instance; + } + + @Override + public boolean isSupport(HttpRequestLine requestLine) { + HttpMethod method = requestLine.getMethod(); + String requestPath = requestLine.getRequestPath(); + + return method == HttpMethod.GET && STATIC_FILE_PATTERN.matcher(requestPath).matches(); + } + + @Override + public HttpResponse handle(HttpRequest request) throws IOException { + HttpResponseHeader responseHeader = new HttpResponseHeader(); + String requestPath = request.getRequestLine().getRequestPath(); + HttpResponseBody responseBody = ResourceLoader.getInstance().loadResource(requestPath); + + return HttpResponse.ok(responseHeader, responseBody); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/handler/RootPageHandler.java b/tomcat/src/main/java/com/techcourse/web/handler/RootPageHandler.java new file mode 100644 index 0000000000..ca940a9330 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/handler/RootPageHandler.java @@ -0,0 +1,39 @@ +package com.techcourse.web.handler; + +import org.apache.coyote.http11.http.request.HttpMethod; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.apache.coyote.http11.http.response.HttpResponseHeader; + +public class RootPageHandler implements Handler { + + private static final RootPageHandler instance = new RootPageHandler(); + private static final String ROOT_PATH = "/"; + private static final String ROOT_PATH_CONTENT = "Hello world!"; + private static final String ROOT_PATH_CONTENT_TYPE = "text/html"; + + private RootPageHandler() { + } + + public static RootPageHandler getInstance() { + return instance; + } + + @Override + public boolean isSupport(HttpRequestLine requestLine) { + HttpMethod method = requestLine.getMethod(); + String requestPath = requestLine.getRequestPath(); + + return method == HttpMethod.GET && requestPath.equals(ROOT_PATH); + } + + @Override + public HttpResponse handle(HttpRequest request) { + HttpResponseHeader responseHeader = new HttpResponseHeader(); + HttpResponseBody responseBody = new HttpResponseBody(ROOT_PATH_CONTENT_TYPE, ROOT_PATH_CONTENT.getBytes()); + + return HttpResponse.ok(responseHeader, responseBody); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/util/FormUrlEncodedParser.java b/tomcat/src/main/java/com/techcourse/web/util/FormUrlEncodedParser.java new file mode 100644 index 0000000000..7928d40dcf --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/util/FormUrlEncodedParser.java @@ -0,0 +1,38 @@ +package com.techcourse.web.util; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class FormUrlEncodedParser { + + private static final String PARAMETER_SEPARATOR = "&"; + private static final String KEY_VALUE_SEPARATOR = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + public static Map parse(String body) { + validateBodyIsNotEmpty(body); + String[] pairs = body.split(PARAMETER_SEPARATOR); + + return Arrays.stream(pairs) + .collect(Collectors.toMap( + pair -> pair.split(KEY_VALUE_SEPARATOR)[KEY_INDEX], + FormUrlEncodedParser::getValue + )); + } + + private static void validateBodyIsNotEmpty(String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("Body is empty"); + } + } + + private static String getValue(String pair) { + String[] keyValue = pair.split(KEY_VALUE_SEPARATOR); + if (keyValue.length == 1) { + return ""; + } + return keyValue[VALUE_INDEX]; + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/util/JsessionIdGenerator.java b/tomcat/src/main/java/com/techcourse/web/util/JsessionIdGenerator.java new file mode 100644 index 0000000000..1f97b5f65c --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/util/JsessionIdGenerator.java @@ -0,0 +1,10 @@ +package com.techcourse.web.util; + +import java.util.UUID; + +public class JsessionIdGenerator { + + public static String generate() { + return UUID.randomUUID().toString(); + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/util/LoginChecker.java b/tomcat/src/main/java/com/techcourse/web/util/LoginChecker.java new file mode 100644 index 0000000000..57d5da10f9 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/util/LoginChecker.java @@ -0,0 +1,24 @@ +package com.techcourse.web.util; + +import org.apache.coyote.http11.http.HttpCookie; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.session.Session; +import org.apache.coyote.http11.http.session.SessionManager; + +public class LoginChecker { + + public static boolean isLoggedIn(HttpRequest request) { + HttpCookie cookie = request.getHeaders().getHttpCookie(); + if (cookie == null) { + return false; + } + + String sessionId = cookie.getJsessionid(); + if (sessionId == null) { + return false; + } + + Session session = SessionManager.getSession(sessionId); + return session != null && session.getUser() != null; + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/util/ResourceLoader.java b/tomcat/src/main/java/com/techcourse/web/util/ResourceLoader.java new file mode 100644 index 0000000000..ed6de270d2 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/web/util/ResourceLoader.java @@ -0,0 +1,36 @@ +package com.techcourse.web.util; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.coyote.http11.http.response.HttpResponseBody; + +public class ResourceLoader { + + private static final ResourceLoader instance = new ResourceLoader(); + private static final String RESOURCE_PATH = "/static"; + + private ResourceLoader() { + } + + public static ResourceLoader getInstance() { + return instance; + } + + public HttpResponseBody loadResource(String filePath) throws IOException { + Path path = getPath(RESOURCE_PATH + filePath); + + return new HttpResponseBody(Files.probeContentType(path), Files.readAllBytes(path)); + } + + private Path getPath(String fileName) { + URL resource = getClass().getResource(fileName); + if (resource == null) { + throw new IllegalArgumentException("Resource not found. resource: " + fileName); + } + + return Path.of(resource.getPath()); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..85345f78ff 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,9 +1,9 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; - import java.io.IOException; +import jakarta.servlet.http.HttpSession; + /** * A Manager manages the pool of Sessions that are associated with a * particular Container. Different Manager implementations may support @@ -24,33 +24,31 @@ */ public interface Manager { - /** - * Add this Session to the set of active Sessions for this Manager. - * - * @param session Session to be added - */ - void add(HttpSession session); + /** + * Add this Session to the set of active Sessions for this Manager. + * + * @param session Session to be added + */ + void add(HttpSession session); - /** - * Return the active Session, associated with this Manager, with the - * specified session id (if any); otherwise return null. - * - * @param id The session id for the session to be returned - * - * @exception IllegalStateException if a new session cannot be - * instantiated for any reason - * @exception IOException if an input/output error occurs while - * processing this request - * - * @return the request session or {@code null} if a session with the - * requested ID could not be found - */ - HttpSession findSession(String id) throws IOException; + /** + * Return the active Session, associated with this Manager, with the + * specified session id (if any); otherwise return null. + * + * @param id The session id for the session to be returned + * @return the request session or {@code null} if a session with the + * requested ID could not be found + * @throws IllegalStateException if a new session cannot be + * instantiated for any reason + * @throws IOException if an input/output error occurs while + * processing this request + */ + HttpSession findSession(String id) throws IOException; - /** - * Remove this Session from the active Sessions for this Manager. - * - * @param session Session to be removed - */ - void remove(HttpSession session); + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + */ + void remove(HttpSession session); } 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..da06e9b179 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,95 +1,95 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class Connector implements Runnable { - private static final Logger log = LoggerFactory.getLogger(Connector.class); - - private static final int DEFAULT_PORT = 8080; - private static final int DEFAULT_ACCEPT_COUNT = 100; - - private final ServerSocket serverSocket; - private boolean stopped; - - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); - } - - public Connector(final int port, final int acceptCount) { - this.serverSocket = createServerSocket(port, acceptCount); - this.stopped = false; - } - - private ServerSocket createServerSocket(final int port, final int acceptCount) { - try { - final int checkedPort = checkPort(port); - final int checkedAcceptCount = checkAcceptCount(acceptCount); - return new ServerSocket(checkedPort, checkedAcceptCount); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public void start() { - var thread = new Thread(this); - thread.setDaemon(true); - thread.start(); - stopped = false; - log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); - } - - @Override - public void run() { - // 클라이언트가 연결될때까지 대기한다. - while (!stopped) { - connect(); - } - } - - private void connect() { - try { - process(serverSocket.accept()); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private void process(final Socket connection) { - if (connection == null) { - return; - } - var processor = new Http11Processor(connection); - new Thread(processor).start(); - } - - public void stop() { - stopped = true; - try { - serverSocket.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private int checkPort(final int port) { - final var MIN_PORT = 1; - final var MAX_PORT = 65535; - - if (port < MIN_PORT || MAX_PORT < port) { - return DEFAULT_PORT; - } - return port; - } - - private int checkAcceptCount(final int acceptCount) { - return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); - } + private static final Logger log = LoggerFactory.getLogger(Connector.class); + + private static final int DEFAULT_PORT = 8080; + private static final int DEFAULT_ACCEPT_COUNT = 100; + + private final ServerSocket serverSocket; + private boolean stopped; + + public Connector() { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); + } + + public Connector(final int port, final int acceptCount) { + this.serverSocket = createServerSocket(port, acceptCount); + this.stopped = false; + } + + private ServerSocket createServerSocket(final int port, final int acceptCount) { + try { + final int checkedPort = checkPort(port); + final int checkedAcceptCount = checkAcceptCount(acceptCount); + return new ServerSocket(checkedPort, checkedAcceptCount); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void start() { + var thread = new Thread(this); + thread.setDaemon(true); + thread.start(); + stopped = false; + log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); + } + + @Override + public void run() { + // 클라이언트가 연결될때까지 대기한다. + while (!stopped) { + connect(); + } + } + + private void connect() { + try { + process(serverSocket.accept()); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private void process(final Socket connection) { + if (connection == null) { + return; + } + var processor = new Http11Processor(connection); + new Thread(processor).start(); + } + + public void stop() { + stopped = true; + try { + serverSocket.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private int checkPort(final int port) { + final var MIN_PORT = 1; + final var MAX_PORT = 65535; + + if (port < MIN_PORT || MAX_PORT < port) { + return DEFAULT_PORT; + } + return port; + } + + private int checkAcceptCount(final int acceptCount) { + return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); + } } 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..f63ab65999 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,27 +1,27 @@ package org.apache.catalina.startup; +import java.io.IOException; + import org.apache.catalina.connector.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class Tomcat { - private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + private static final Logger log = LoggerFactory.getLogger(Tomcat.class); - public void start() { - var connector = new Connector(); - connector.start(); + public void start() { + var connector = new Connector(); + connector.start(); - try { - // make the application wait until we press any key. - System.in.read(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } finally { - log.info("web server stop."); - connector.stop(); - } - } + try { + // make the application wait until we press any key. + System.in.read(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + log.info("web server stop."); + connector.stop(); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/Processor.java b/tomcat/src/main/java/org/apache/coyote/Processor.java index 6604ab83de..47ab0b2dbc 100644 --- a/tomcat/src/main/java/org/apache/coyote/Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/Processor.java @@ -23,10 +23,10 @@ */ public interface Processor { - /** - * Process a connection. This is called whenever an event occurs (e.g. more - * data arrives) that allows processing to continue for a connection that is - * not currently being processed. - */ - void process(Socket socket); + /** + * Process a connection. This is called whenever an event occurs (e.g. more + * data arrives) that allows processing to continue for a connection that is + * not currently being processed. + */ + void process(Socket socket); } 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 7c0429258d..5260eb7a15 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,28 +1,27 @@ package org.apache.coyote.http11; import java.io.IOException; +import java.io.InputStream; import java.net.Socket; -import java.nio.charset.StandardCharsets; import org.apache.coyote.Processor; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.techcourse.exception.UncheckedServletException; -import com.techcourse.web.HttpResponse; -import com.techcourse.web.handler.RequestHandler; -import com.techcourse.web.handler.RequestHandlerMapper; +import com.techcourse.web.handler.Handler; +import com.techcourse.web.handler.HandlerMapper; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; - private final RequestHandlerMapper handlerMapper; public Http11Processor(final Socket connection) { this.connection = connection; - this.handlerMapper = RequestHandlerMapper.getInstance(); } @Override @@ -36,15 +35,20 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - HttpRequest request = HttpRequestReader.read(inputStream); - RequestHandler handler = handlerMapper.findHandler(request.getRequestPath()); - HttpResponse response = handler.handle(request); - String httpResponseMessage = response.createResponseMessage(); + String httpResponseMessage = createHttpResponseMessage(inputStream); - outputStream.write(httpResponseMessage.getBytes(StandardCharsets.UTF_8)); + outputStream.write(httpResponseMessage.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private String createHttpResponseMessage(InputStream inputStream) throws IOException { + HttpRequest request = HttpRequestMessageReader.read(inputStream); + Handler handler = HandlerMapper.findHandler(request); + HttpResponse response = handler.handle(request); + + return response.toResponseMessage(); + } } 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 adf9646402..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.apache.coyote.http11; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class HttpRequest { - - private final String method; - private final HttpRequestTarget requestTarget; - private final String protocol; - private final Map headers; - private final String body; - - public HttpRequest(List httpRequest) { - String[] requestLine = httpRequest.getFirst().split(" "); - this.method = requestLine[0]; - this.requestTarget = new HttpRequestTarget(requestLine[1]); - this.protocol = requestLine[2]; - this.headers = createHeaders(httpRequest); - this.body = createBody(httpRequest); - } - - private Map createHeaders(List httpRequest) { - Map headers = new HashMap<>(); - httpRequest.stream() - .skip(1) - .takeWhile(header -> !header.isBlank()) - .forEach(header -> { - String[] keyValue = header.split(":", 2); - headers.put(keyValue[0], keyValue[1].strip()); - }); - return headers; - } - - private String createBody(List httpRequest) { - return httpRequest.stream() - .skip(1) - .dropWhile(header -> !header.isBlank()) - .collect(Collectors.joining("\r\n")); - } - - public String getMethod() { - return method; - } - - public String getRequestPath() { - return requestTarget.path; - } - - public Map getRequestQuery() { - return requestTarget.query; - } - - public String getProtocol() { - return protocol; - } - - public Map getHeaders() { - return headers; - } - - public String getBody() { - return body; - } - - @Override - public String toString() { - return "HttpRequest{" + - "method='" + method + '\'' + - ", requestTarget=" + requestTarget + - ", protocol='" + protocol + '\'' + - ", headers=" + headers + - ", body='" + body + '\'' + - '}'; - } - - static class HttpRequestTarget { - - private final String path; - private final Map query; - - public HttpRequestTarget(String requestTarget) { - String[] parts = requestTarget.split("\\?"); - String query = parts.length == 2 ? parts[1] : ""; - this.path = parts[0]; - this.query = createQuery(query); - } - - private Map createQuery(String query) { - Map queries = new HashMap<>(); - - if (query.isBlank()) { - return queries; - } - - String[] parts = query.split("&"); - for (String part : parts) { - String[] keyValue = part.split("="); - queries.put(keyValue[0], keyValue[1]); - } - return queries; - } - - @Override - public String toString() { - return "HttpRequestTarget{" + - "path='" + path + '\'' + - ", query=" + query + - '}'; - } - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestMessageReader.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestMessageReader.java new file mode 100644 index 0000000000..f8c8015759 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestMessageReader.java @@ -0,0 +1,61 @@ +package org.apache.coyote.http11; + +import static org.apache.coyote.http11.http.BaseHttpHeaders.*; +import static org.apache.coyote.http11.http.request.HttpRequestHeader.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.coyote.http11.http.HttpHeader; +import org.apache.coyote.http11.http.request.HttpRequest; + +public class HttpRequestMessageReader { + + public static HttpRequest read(InputStream inputStream) throws IOException { + + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String requestLine = reader.readLine(); + List headers = readHeaders(reader); + String body = readBody(reader, getContentLength(headers)); + + return new HttpRequest(requestLine, headers, body); + } + + private static int getContentLength(List headers) { + return headers.stream() + .filter(header -> header.startsWith(HttpHeader.CONTENT_LENGTH.getName())) + .findFirst() + .map(header -> Integer.parseInt(header.split(HEADER_DELIMITER)[HEADER_VALUE_INDEX].strip())) + .orElse(0); + } + + private static String readBody(BufferedReader reader, int contentLength) throws IOException { + if (contentLength == 0) { + return ""; + } + + char[] body = new char[contentLength]; + int readLength = reader.read(body, 0, contentLength); + if (readLength != contentLength) { + throw new IllegalArgumentException("Content-Length mismatch"); + } + + return new String(body); + } + + private static List readHeaders(BufferedReader reader) throws IOException { + List headers = new ArrayList<>(); + + String line = reader.readLine(); + while (line != null && !line.isEmpty()) { + headers.add(line); + line = reader.readLine(); + } + + return headers; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestReader.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestReader.java deleted file mode 100644 index 77a22a4812..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequestReader.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.apache.coyote.http11; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -public class HttpRequestReader { - - public static HttpRequest read(InputStream inputStream) throws IOException { - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(reader); - - List httpRequest = new ArrayList<>(); - - String line; - int contentLength = 0; - - while ((line = br.readLine()) != null && !line.isEmpty()) { - httpRequest.add(line); - - if (line.startsWith("Content-Length:")) { - contentLength = Integer.parseInt(line.split(":")[1].strip()); - } - } - - if (contentLength > 0) { - httpRequest.add(""); - char[] body = new char[contentLength]; - int numberOfCharacterRead = br.read(body, 0, contentLength); - String requestBody = new String(body, 0, numberOfCharacterRead); - httpRequest.add(requestBody); - } - - return new HttpRequest(httpRequest); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/BaseHttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/http/BaseHttpHeaders.java new file mode 100644 index 0000000000..140df10847 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/BaseHttpHeaders.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.http; + +import java.util.List; +import java.util.Map; + +public abstract class BaseHttpHeaders { + + public static final String HEADER_DELIMITER = ": "; + public static final String HEADER_VALUE_DELIMITER = ", "; + + protected final Map> headers; + + public BaseHttpHeaders(Map> headers) { + this.headers = headers; + } + + public List getValue(String key) { + if (isNotExistKey(key)) { + throw new IllegalArgumentException("Key not found. key: " + key); + } + return headers.get(key); + } + + private boolean isNotExistKey(String key) { + if (headers == null) { + return false; + } + return !headers.containsKey(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpCookie.java new file mode 100644 index 0000000000..93a58ae953 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpCookie.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11.http; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpCookie { + + private static final String JSESSIONID = "JSESSIONID"; + private static final String COOKIE_VALUE_DELIMITER = "; "; + private static final String KEY_VALUE_DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map cookie; + + public static HttpCookie from(String cookieValues) { + String[] cookies = cookieValues.split(COOKIE_VALUE_DELIMITER); + + return Arrays.stream(cookies).collect(Collectors.collectingAndThen( + Collectors.toMap( + c -> c.split(KEY_VALUE_DELIMITER)[KEY_INDEX], + c -> c.split(KEY_VALUE_DELIMITER)[VALUE_INDEX] + ), + HttpCookie::new)); + } + + private HttpCookie(Map cookie) { + this.cookie = cookie; + } + + public String getValue(String key) { + if (isNotExistKey(key)) { + throw new IllegalArgumentException("Key not found. key: " + key); + } + return cookie.get(key); + } + + private boolean isNotExistKey(String key) { + return !cookie.containsKey(key); + } + + public boolean hasJsessionId() { + return cookie.containsKey(JSESSIONID) && cookie.get(JSESSIONID) != null; + } + + public String getJsessionid() { + return getValue(JSESSIONID); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/HttpHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpHeader.java new file mode 100644 index 0000000000..9106322959 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/HttpHeader.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.http; + +public enum HttpHeader { + + LOCATION("Location"), + COOKIE("Cookie"), + SET_COOKIE("Set-Cookie"), + CONTENT_TYPE("Content-Type"), + CONTENT_LENGTH("Content-Length"), + ; + + private final String name; + + HttpHeader(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpMethod.java new file mode 100644 index 0000000000..60d084f61a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpMethod.java @@ -0,0 +1,9 @@ +package org.apache.coyote.http11.http.request; + +public enum HttpMethod { + GET, POST; + + public static HttpMethod from(String method) { + return HttpMethod.valueOf(method); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequest.java new file mode 100644 index 0000000000..46ab9e4ace --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequest.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.http.request; + +import java.util.List; + +public class HttpRequest { + + private final HttpRequestLine requestLine; + private final HttpRequestHeader headers; + private final String requestBody; + + public HttpRequest(String requestLine, List headers, String requestBody) { + this.requestLine = HttpRequestLine.from(requestLine); + this.headers = HttpRequestHeader.from(headers); + this.requestBody = requestBody; + } + + public HttpRequestLine getRequestLine() { + return requestLine; + } + + public HttpRequestHeader getHeaders() { + return headers; + } + + public String getRequestBody() { + return requestBody; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestHeader.java new file mode 100644 index 0000000000..9f6b84e448 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestHeader.java @@ -0,0 +1,76 @@ +package org.apache.coyote.http11.http.request; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.coyote.http11.http.BaseHttpHeaders; +import org.apache.coyote.http11.http.HttpCookie; +import org.apache.coyote.http11.http.HttpHeader; + +public class HttpRequestHeader extends BaseHttpHeaders { + + public static final int HEADER_KEY_INDEX = 0; + public static final int HEADER_VALUE_INDEX = 1; + + private final HttpCookie httpCookie; + + public HttpRequestHeader(Map> headers, HttpCookie httpCookie) { + super(headers); + this.httpCookie = httpCookie; + } + + public static HttpRequestHeader from(List headers) { + return new HttpRequestHeader(initHeaders(headers), initCookie(headers)); + } + + private static Map> initHeaders(List headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + + Map> result = new LinkedHashMap<>(); + headers.stream() + .filter(header -> !header.startsWith(HttpHeader.COOKIE.getName())) + .forEach(h -> { + String[] headerParts = h.split(HEADER_DELIMITER); + result.put(headerParts[HEADER_KEY_INDEX], parseHeaderValue(h, headerParts[HEADER_VALUE_INDEX])); + }); + + return result; + } + + private static List parseHeaderValue(String header, String headerPart) { + if (header.startsWith(HttpHeader.COOKIE.getName())) { + return List.of(headerPart); + } + return Arrays.stream(headerPart.split(HEADER_VALUE_DELIMITER)) + .map(String::strip) + .toList(); + } + + private static HttpCookie initCookie(List headers) { + String cookieHeader = getCookieHeader(headers); + if (cookieHeader == null) { + return null; + } + String cookieValues = cookieHeader.split(HEADER_DELIMITER)[HEADER_VALUE_INDEX]; + return HttpCookie.from(cookieValues); + } + + private static String getCookieHeader(List headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + + return headers.stream() + .filter(header -> header.startsWith(HttpHeader.COOKIE.getName())) + .findFirst() + .orElse(null); + } + + public HttpCookie getHttpCookie() { + return httpCookie; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestLine.java new file mode 100644 index 0000000000..64998874bf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestLine.java @@ -0,0 +1,48 @@ +package org.apache.coyote.http11.http.request; + +public class HttpRequestLine { + + private static final String PART_DELIMITER = " "; + private static final int METHOD_INDEX = 0; + private static final int REQUEST_URL_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + + private final HttpMethod method; + private final HttpRequestUrl httpRequestUrl; + private final String httpVersion; + + public HttpRequestLine(String method, HttpRequestUrl httpRequestUrl, String httpVersion) { + this.method = HttpMethod.valueOf(method); + this.httpRequestUrl = httpRequestUrl; + this.httpVersion = httpVersion; + } + + public static HttpRequestLine from(String requestLine) { + String[] parts = requestLine.split(PART_DELIMITER); + return new HttpRequestLine( + parts[METHOD_INDEX], + HttpRequestUrl.from(parts[REQUEST_URL_INDEX]), + parts[HTTP_VERSION_INDEX] + ); + } + + public HttpMethod getMethod() { + return method; + } + + public String getRequestPath() { + return httpRequestUrl.getPath(); + } + + public HttpRequestQuery getQuery() { + return httpRequestUrl.getQuery(); + } + + public HttpRequestUrl getHttpRequestUrl() { + return httpRequestUrl; + } + + public String getHttpVersion() { + return httpVersion; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestQuery.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestQuery.java new file mode 100644 index 0000000000..95259abd8c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestQuery.java @@ -0,0 +1,74 @@ +package org.apache.coyote.http11.http.request; + +import static org.reflections.Reflections.*; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequestQuery { + + private static final String QUERY_PARAM_DELIMITER = "&"; + private static final String QUERY_KEY_VALUE_DELIMITER = "="; + private static final int QUERY_KEY_INDEX = 0; + private static final int QUERY_VALUE_INDEX = 1; + + private final Map query; + + public HttpRequestQuery(Map query) { + this.query = query; + } + + public static HttpRequestQuery from(String query) { + return new HttpRequestQuery(initQuery(query)); + } + + private static Map initQuery(String query) { + if (query == null || query.isBlank()) { + return null; + } + + return Arrays.stream(query.split(QUERY_PARAM_DELIMITER)) + .collect(Collectors.toMap(HttpRequestQuery::parseKey, HttpRequestQuery::parseValue)); + } + + private static String parseKey(String queryPart) { + return queryPart.split(QUERY_KEY_VALUE_DELIMITER)[QUERY_KEY_INDEX]; + } + + private static String parseValue(String queryPart) { + String[] parts = queryPart.split(QUERY_KEY_VALUE_DELIMITER); + if (parts.length == 1) { + return ""; + } + return parts[QUERY_VALUE_INDEX]; + } + + public String getValue(String key) { + if (isNotExistKey(key)) { + log.info("Key not found: {}", key); + return null; + } + return query.get(key); + } + + private boolean isNotExistKey(String key) { + if (query == null) { + return false; + } + return !query.containsKey(key); + } + + public boolean isExist() { + return query != null; + } + + public String toUrl() { + if (query == null) { + return ""; + } + return query.entrySet().stream() + .map(e -> e.getKey() + QUERY_KEY_VALUE_DELIMITER + e.getValue()) + .collect(Collectors.joining(QUERY_PARAM_DELIMITER)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestUrl.java b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestUrl.java new file mode 100644 index 0000000000..63729f7d78 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/request/HttpRequestUrl.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.http.request; + +public class HttpRequestUrl { + + private static final String REQUEST_URL_DELIMITER = "\\?"; + private static final int REQUEST_PATH_INDEX = 0; + private static final int REQUEST_QUERY_INDEX = 1; + + private final String path; + private final HttpRequestQuery httpRequestQuery; + + public static HttpRequestUrl from(String requestUrl) { + String[] urlParts = requestUrl.split(REQUEST_URL_DELIMITER); + if (urlParts.length == 1) { + return new HttpRequestUrl(urlParts[REQUEST_PATH_INDEX], null); + } + return new HttpRequestUrl(urlParts[REQUEST_PATH_INDEX], urlParts[REQUEST_QUERY_INDEX]); + } + + public HttpRequestUrl(String path, String query) { + this.path = path; + this.httpRequestQuery = HttpRequestQuery.from(query); + } + + public boolean isQueryExist() { + return httpRequestQuery.isExist(); + } + + public String getPath() { + return path; + } + + public HttpRequestQuery getQuery() { + return httpRequestQuery; + } + + public String getRequestUrl() { + if (httpRequestQuery.isExist()) { + return path + "?" + httpRequestQuery.toUrl(); + } + return path; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponse.java new file mode 100644 index 0000000000..f7d9ec40f1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponse.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11.http.response; + +import org.apache.coyote.http11.http.HttpHeader; + +public class HttpResponse { + + private static final String HTTP_VERSION = "HTTP/1.1"; + + private final HttpResponseStartLine startLine; + private final HttpResponseHeader header; + private final HttpResponseBody body; + + public static HttpResponse ok(HttpResponseHeader header, HttpResponseBody body) { + return new HttpResponse(new HttpResponseStartLine(HTTP_VERSION, HttpStatusCode.OK), header, body); + } + + public static HttpResponse notFound(HttpResponseHeader header, HttpResponseBody body) { + return new HttpResponse(new HttpResponseStartLine(HTTP_VERSION, HttpStatusCode.NOT_FOUND), header, body); + } + + public static HttpResponse redirect(HttpResponseHeader responseHeader) { + return new HttpResponse(new HttpResponseStartLine(HTTP_VERSION, HttpStatusCode.FOUND), responseHeader, null); + } + + public static HttpResponse badRequest(HttpResponseHeader httpResponseHeader) { + return new HttpResponse(new HttpResponseStartLine(HTTP_VERSION, HttpStatusCode.BAD_REQUEST), httpResponseHeader, + null); + } + + private HttpResponse(HttpResponseStartLine startLine, HttpResponseHeader header, HttpResponseBody body) { + this.startLine = startLine; + this.header = header; + this.body = body; + } + + public String toResponseMessage() { + if (body == null) { + return createResponseMessageWithoutBody(); + } + + return createResponseMessage(); + } + + private String createResponseMessageWithoutBody() { + header.addHeader(HttpHeader.CONTENT_LENGTH.getName(), "0"); + return String.join("\r\n" + , startLine.toResponseMessage() + , header.toResponseMessage() + , "" + ); + } + + private String createResponseMessage() { + String contentCharset = ";charset=utf-8"; + header.addHeader(HttpHeader.CONTENT_TYPE.getName(), body.getContentType() + contentCharset); + header.addHeader(HttpHeader.CONTENT_LENGTH.getName(), String.valueOf(body.getContentLength())); + + return String.join("\r\n" + , startLine.toResponseMessage() + , header.toResponseMessage() + , "" + , body.toResponseMessage() + ); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseBody.java b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseBody.java new file mode 100644 index 0000000000..cbd99fa774 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseBody.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.http.response; + +import java.nio.charset.StandardCharsets; + +public class HttpResponseBody { + + private final String contentType; + private final byte[] content; + + public HttpResponseBody(String contentType, byte[] content) { + this.contentType = contentType; + this.content = content; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } + + public int getContentLength() { + return content.length; + } + + public String toResponseMessage() { + return new String(content, StandardCharsets.UTF_8); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseHeader.java new file mode 100644 index 0000000000..d3b75ec062 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseHeader.java @@ -0,0 +1,39 @@ +package org.apache.coyote.http11.http.response; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import org.apache.coyote.http11.http.BaseHttpHeaders; +import org.apache.coyote.http11.http.HttpHeader; + +public class HttpResponseHeader extends BaseHttpHeaders { + + private static final String JSESSIONID = "JSESSIONID="; + + public HttpResponseHeader() { + super(new LinkedHashMap<>()); + } + + public void addHeader(String key, String value) { + addHeader(key, List.of(value)); + } + + public void addHeader(String key, List values) { + headers.put(key, values); + } + + public void addJSessionId(String sessionId) { + addHeader(HttpHeader.SET_COOKIE.getName(), JSESSIONID + sessionId); + } + + public String toResponseMessage() { + List lines = new ArrayList<>(); + headers.forEach((key, value) -> { + String values = String.join(HEADER_VALUE_DELIMITER, value); + lines.add(key + HEADER_DELIMITER + values + " "); + }); + + return String.join("\r\n", lines); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseStartLine.java new file mode 100644 index 0000000000..15800e3441 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpResponseStartLine.java @@ -0,0 +1,16 @@ +package org.apache.coyote.http11.http.response; + +public class HttpResponseStartLine { + + private final String httpVersion; + private final HttpStatusCode statusCode; + + public HttpResponseStartLine(String httpVersion, HttpStatusCode statusCode) { + this.httpVersion = httpVersion; + this.statusCode = statusCode; + } + + public String toResponseMessage() { + return httpVersion + " " + statusCode.getCode() + " " + statusCode.getMessage() + " "; + } +} diff --git a/tomcat/src/main/java/com/techcourse/web/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpStatusCode.java similarity index 73% rename from tomcat/src/main/java/com/techcourse/web/HttpStatusCode.java rename to tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpStatusCode.java index d11033b1b6..91391be3cb 100644 --- a/tomcat/src/main/java/com/techcourse/web/HttpStatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/response/HttpStatusCode.java @@ -1,16 +1,16 @@ -package com.techcourse.web; +package org.apache.coyote.http11.http.response; public enum HttpStatusCode { - // success OK(200, "OK"), - // client error + FOUND(302, "Found"), + BAD_REQUEST(400, "Bad Request"), NOT_FOUND(404, "Not Found"), - // server error, - INTERNAL_SERVER_ERROR(500, "Internal Server Error"); + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + ; private final int code; private final String message; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/session/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/http/session/Session.java new file mode 100644 index 0000000000..fafbcd5a3e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/session/Session.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.http.session; + +import com.techcourse.model.User; + +public class Session { + + private final String id; + private final User user; + + public Session(String id, User user) { + this.id = id; + this.user = user; + } + + public String getId() { + return id; + } + + public User getUser() { + return user; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/http/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/http/session/SessionManager.java new file mode 100644 index 0000000000..a367a2465f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/http/session/SessionManager.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.http.session; + +import java.util.HashMap; +import java.util.Map; + +import com.techcourse.model.User; + +public class SessionManager { + + private static final Map sessions = new HashMap<>(); + + public static void createSession(String id, User user) { + sessions.put(id, new Session(id, user)); + } + + public static void removeSession(String sessionId) { + sessions.remove(sessionId); + } + + public static Session getSession(String sessionId) { + return sessions.get(sessionId); + } +} diff --git a/tomcat/src/main/resources/static/favicon.ico b/tomcat/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000..ceb052efc9 Binary files /dev/null and b/tomcat/src/main/resources/static/favicon.ico differ diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
diff --git a/tomcat/src/test/java/com/techcourse/db/InMemoryUserRepositoryTest.java b/tomcat/src/test/java/com/techcourse/db/InMemoryUserRepositoryTest.java new file mode 100644 index 0000000000..227684b282 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/db/InMemoryUserRepositoryTest.java @@ -0,0 +1,21 @@ +package com.techcourse.db; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.techcourse.model.User; + +class InMemoryUserRepositoryTest { + + @DisplayName("이미 존재하는 계정이면 예외를 던진다.") + @Test + void save_whenExistingAccount() { + User user = new User("gugu", "password", "hi@hi.com"); + + assertThatThrownBy(() -> InMemoryUserRepository.save(user)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("account is already exist"); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/HandlerMapperTest.java b/tomcat/src/test/java/com/techcourse/web/handler/HandlerMapperTest.java new file mode 100644 index 0000000000..34b2a9fbed --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/handler/HandlerMapperTest.java @@ -0,0 +1,60 @@ +package com.techcourse.web.handler; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HandlerMapperTest { + + @DisplayName("/ 요청에는 RootPageHandler를 반환한다.") + @Test + void findHandler_WhenRequestRootPage() { + String requestPath = "/"; + + Class expectedHandler = RootPageHandler.class; + + runTest(requestPath, expectedHandler); + } + + @DisplayName("/login 요청에는 LoginHandler를 반환한다.") + @Test + void findHandler_WhenRequestLogin() { + String requestPath = "/login"; + + Class expectedHandler = LoginHandler.class; + + runTest(requestPath, expectedHandler); + } + + @DisplayName("정적 리소스 요청에는 ResourceHandler를 반환한다.") + @Test + void findHandler_WhenRequestResource() { + String requestPath = "/css/test.css"; + + Class expectedHandler = ResourceHandler.class; + + runTest(requestPath, expectedHandler); + } + + @DisplayName("찾는 핸들러가 없는 경우 404 핸들러를 반환한다.") + @Test + void findHandler_WhenHandlerNotFound() { + String requestPath = "/notfound"; + + Class expectedHandler = NotFoundHandler.class; + + runTest(requestPath, expectedHandler); + } + + private void runTest(String requestPath, Class expectedHandler) { + HttpRequest request = new HttpRequest("GET " + requestPath + " HTTP/1.1", List.of(), null); + + Handler handler = HandlerMapper.findHandler(request); + + assertThat(handler).isExactlyInstanceOf(expectedHandler); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/LoginHandlerTest.java b/tomcat/src/test/java/com/techcourse/web/handler/LoginHandlerTest.java index 0ed637f3ab..31b8cf76ae 100644 --- a/tomcat/src/test/java/com/techcourse/web/handler/LoginHandlerTest.java +++ b/tomcat/src/test/java/com/techcourse/web/handler/LoginHandlerTest.java @@ -3,40 +3,115 @@ import static org.assertj.core.api.Assertions.*; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; +import java.util.List; +import java.util.Map; -import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.request.HttpRequestUrl; +import org.apache.coyote.http11.http.response.HttpStatusCode; +import org.assertj.core.api.Condition; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import com.techcourse.web.HttpResponse; +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import org.apache.coyote.http11.http.session.SessionManager; +import com.techcourse.web.util.JsessionIdGenerator; class LoginHandlerTest { + @DisplayName("/login 으로 시작하는 페이지를 처리한다.") + @Test + void isSupport() { + HttpRequestUrl requestUrl = HttpRequestUrl.from("/login?account=hi"); + HttpRequestLine requestLine = new HttpRequestLine("GET", requestUrl, "HTTP/1.1"); + LoginHandler loginHandler = LoginHandler.getInstance(); + + boolean isSupport = loginHandler.isSupport(requestLine); + + assertThat(isSupport).isTrue(); + } + + @DisplayName("이미 로그인 된 회원이 GET /login 요청을 보내면 /index.html로 리다이렉트한다.") + @Test + void alreadyLoggedIn() throws IOException { + User user = InMemoryUserRepository.findByAccount("gugu").get(); + String sessionId = JsessionIdGenerator.generate(); + SessionManager.createSession(sessionId, user); + + List headers = List.of("Host: example.com", "Accept: text/html", "Cookie: JSESSIONID=" + sessionId); + HttpRequest request = new HttpRequest("GET /login HTTP/1.1", headers, null); - @DisplayName("/login?account={account}&password={password} 경로 요청에 대한 응답을 생성한다.") + LoginHandler loginHandler = LoginHandler.getInstance(); + + assertThat(loginHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(loginHandler.handle(request)) + .extracting("header") + .extracting("headers") + .extracting("Location") + .isEqualTo(List.of("/index.html")); + } + + @DisplayName("로그인에 성공하면 /index.html로 리다이렉트한다.") @Test - void loginWithQuery() throws IOException { - HttpRequest httpRequest = new HttpRequest(Arrays.asList( - "GET /login?account=gugu&password=password HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "Accept: text/html " - )); - - - HttpResponse response = LoginHandler.getInstance().loginWithQuery(httpRequest); - Path path = Path.of("src/main/resources/static/login.html"); - String content = Files.readString(path); - - assertThat(response).satisfies(r -> { - assertThat(r.getProtocol()).isEqualTo("HTTP/1.1"); - assertThat(r.getStatusCode().getCode()).isEqualTo(200); - assertThat(r.getHeaders()).containsEntry("Content-Type", "text/html;charset=utf-8"); - assertThat(r.getHeaders()).containsEntry("Content-Length", String.valueOf(content.getBytes().length)); - assertThat(r.getBody().getContent()).isEqualTo(content); - }); + void succeedLogin() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("POST /login HTTP/1.1", headers, "account=gugu&password=password"); + + LoginHandler loginHandler = LoginHandler.getInstance(); + + assertThat(loginHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(loginHandler.handle(request)) + .extracting("header") + .extracting("headers") + .extracting("Location") + .isEqualTo(List.of("/index.html")); + } + + @DisplayName("로그인에 성공하고, 쿠키가 존재하지 않으면 쿠키를 생성한다.") + @Test + void createCookie() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("POST /login HTTP/1.1", headers, "account=gugu&password=password"); + + LoginHandler loginHandler = LoginHandler.getInstance(); + + assertThat(loginHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(loginHandler.handle(request)) + .extracting("header") + .extracting("headers") + .extracting("Set-Cookie") + .isNotNull(); + } + + @DisplayName("로그인에 성공해도 쿠키가 존재하면 쿠키를 생성하지 않는다.") + @Test + void notCreateCookie() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html", "Cookie: JSESSIONID=1234"); + HttpRequest request = new HttpRequest("POST /login HTTP/1.1", headers, "account=gugu&password=password"); + + LoginHandler loginHandler = LoginHandler.getInstance(); + + assertThat(loginHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(loginHandler.handle(request)).extracting("header") + .extracting("headers") + .doesNotHave(new Condition<>(h -> ((Map)h).containsKey("Set-Cookie"), "Set-Cookie")); + } + + @DisplayName("로그인에 실패하면 /401.html 로 리다이렉트한다.") + @Test + void handle_whenLoginFailed() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("POST /login HTTP/1.1", headers, "account=hi&password=hello"); + + LoginHandler loginHandler = LoginHandler.getInstance(); + + assertThat(loginHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(loginHandler.handle(request)) + .extracting("header") + .extracting("headers") + .extracting("Location") + .isEqualTo(List.of("/401.html")); } } diff --git a/tomcat/src/test/java/com/techcourse/web/handler/PageHandlerTest.java b/tomcat/src/test/java/com/techcourse/web/handler/PageHandlerTest.java deleted file mode 100644 index 10e85ea2b2..0000000000 --- a/tomcat/src/test/java/com/techcourse/web/handler/PageHandlerTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.techcourse.web.handler; - -import static org.assertj.core.api.Assertions.*; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; - -import org.apache.coyote.http11.HttpRequest; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import com.techcourse.web.HttpResponse; - -class PageHandlerTest { - - @DisplayName("/ 경로 요청에 대한 응답을 생성한다.") - @Test - void getResource_WhenRequestPathIsRoot() { - PageHandler handler = PageHandler.getInstance(); - - HttpRequest httpRequest = new HttpRequest(Arrays.asList( - "GET / HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "Accept: text/html " - )); - - HttpResponse response = handler.handle(httpRequest); - - assertThat(response).satisfies(r -> { - assertThat(r.getProtocol()).isEqualTo("HTTP/1.1"); - assertThat(r.getStatusCode().getCode()).isEqualTo(200); - assertThat(r.getHeaders()).containsEntry("Content-Type", "text/html;charset=utf-8"); - assertThat(r.getHeaders()).containsEntry("Content-Length", "12"); - assertThat(r.getBody().getContent()).isEqualTo("Hello world!"); - }); - } - - @DisplayName("/index.html 경로 요청에 대한 응답을 생성한다.") - @Test - void getResource_WhenRequestPathIsIndexHtml() throws IOException { - PageHandler handler = PageHandler.getInstance(); - - HttpRequest httpRequest = new HttpRequest(Arrays.asList( - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "Accept: text/html " - )); - - HttpResponse response = handler.handle(httpRequest); - Path path = Path.of("src/main/resources/static/index.html"); - String content = Files.readString(path); - - assertThat(response).satisfies(r -> { - assertThat(r.getProtocol()).isEqualTo("HTTP/1.1"); - assertThat(r.getStatusCode().getCode()).isEqualTo(200); - assertThat(r.getHeaders()).containsEntry("Content-Type", "text/html;charset=utf-8"); - assertThat(r.getHeaders()).containsEntry("Content-Length", String.valueOf(content.getBytes().length)); - assertThat(r.getBody().getContent()).isEqualTo(content); - }); - } -} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/RegisterHandlerTest.java b/tomcat/src/test/java/com/techcourse/web/handler/RegisterHandlerTest.java new file mode 100644 index 0000000000..f9aab73901 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/handler/RegisterHandlerTest.java @@ -0,0 +1,85 @@ +package com.techcourse.web.handler; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.util.List; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.request.HttpRequestUrl; +import org.apache.coyote.http11.http.response.HttpStatusCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RegisterHandlerTest { + + @DisplayName("GET /register 요청을 처리한다.") + @Test + void isSupport() { + HttpRequestUrl requestUrl = HttpRequestUrl.from("/register"); + HttpRequestLine requestLine = new HttpRequestLine("GET", requestUrl, "HTTP/1.1"); + RegisterHandler handler = RegisterHandler.getInstance(); + + boolean isSupport = handler.isSupport(requestLine); + + assertThat(isSupport).isTrue(); + } + + @DisplayName("회원가입에 성공한다.") + @Test + void register() throws IOException { + String body = "account=sangdol&password=123&email=hello@naver.com"; + HttpRequest request = new HttpRequest("POST /register HTTP/1.1", List.of(), body); + RegisterHandler handler = RegisterHandler.getInstance(); + + assertThat(handler.isSupport(request.getRequestLine())).isTrue(); + assertThat(handler.handle(request)) + .extracting("header") + .extracting("headers") + .extracting("Location") + .isEqualTo(List.of("/index.html")); + } + + @DisplayName("이미 존재하는 계정이면 400 에러를 반환한다.") + @Test + void register_whenExistingAccount() throws IOException { + String body = "account=gugu&password=123&email=hello@naver.coom"; + HttpRequest request = new HttpRequest("POST /register HTTP/1.1", List.of(), body); + RegisterHandler handler = RegisterHandler.getInstance(); + + assertThat(handler.isSupport(request.getRequestLine())).isTrue(); + assertThat(handler.handle(request)) + .extracting("startLine") + .extracting("statusCode") + .isEqualTo(HttpStatusCode.BAD_REQUEST); + } + + @DisplayName("회원가입을 위해서는 모든 값이 필요하다.") + @Test + void register_whenNotExistValue() throws IOException { + String body = "account=123&password=123"; + HttpRequest request = new HttpRequest("POST /register HTTP/1.1", List.of(), body); + RegisterHandler handler = RegisterHandler.getInstance(); + + assertThat(handler.isSupport(request.getRequestLine())).isTrue(); + assertThat(handler.handle(request)) + .extracting("startLine") + .extracting("statusCode") + .isEqualTo(HttpStatusCode.BAD_REQUEST); + } + + @DisplayName("빈 값을 입력할 수 없다.") + @Test + void register_whenEmptyValue() throws IOException { + String body = "account=123&password= &email=email@email.com"; + HttpRequest request = new HttpRequest("POST /register HTTP/1.1", List.of(), body); + RegisterHandler handler = RegisterHandler.getInstance(); + + assertThat(handler.isSupport(request.getRequestLine())).isTrue(); + assertThat(handler.handle(request)) + .extracting("startLine") + .extracting("statusCode") + .isEqualTo(HttpStatusCode.BAD_REQUEST); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/RequestHandlerMapperTest.java b/tomcat/src/test/java/com/techcourse/web/handler/RequestHandlerMapperTest.java deleted file mode 100644 index 0c70913305..0000000000 --- a/tomcat/src/test/java/com/techcourse/web/handler/RequestHandlerMapperTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.techcourse.web.handler; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class RequestHandlerMapperTest { - - @DisplayName("/login 경로에 대한 핸들러를 찾는다.") - @ParameterizedTest - @ValueSource(strings = {"/login", "/login?account=account", "/login/1"}) - void findLoginHandler(String value) { - - RequestHandler handler = RequestHandlerMapper.getInstance().findHandler(value); - - assertThat(handler).isInstanceOf(LoginHandler.class); - } - - @DisplayName("/ 경로에 대한 핸들러를 찾는다.") - @Test - void findPageHandler() { - RequestHandler handler = RequestHandlerMapper.getInstance().findHandler("/"); - - assertThat(handler).isInstanceOf(PageHandler.class); - } - - @DisplayName("정적 파일에 대한 핸들러를 찾는다.") - @ParameterizedTest - @ValueSource(strings = {"/login.html", "/index.html", "/style.css", "/script.js", "/favicon.ico"}) - void findStaticFileHandler(String value) { - RequestHandler handler = RequestHandlerMapper.getInstance().findHandler(value); - - assertThat(handler).isInstanceOf(PageHandler.class); - } -} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/ResourceHandlerTest.java b/tomcat/src/test/java/com/techcourse/web/handler/ResourceHandlerTest.java new file mode 100644 index 0000000000..371b139fc9 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/handler/ResourceHandlerTest.java @@ -0,0 +1,58 @@ +package com.techcourse.web.handler; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.List; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.request.HttpRequestUrl; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpStatusCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResourceHandlerTest { + + + @DisplayName("정적 리소스 요청을 처리할 수 있다.") + @Test + void isSupport() { + HttpRequestUrl requestUrl = HttpRequestUrl.from("/css/test.css"); + HttpRequestLine requestLine = new HttpRequestLine("GET", requestUrl, "HTTP/1.1"); + ResourceHandler handler = ResourceHandler.getInstance(); + + boolean isSupport = handler.isSupport(requestLine); + + assertThat(isSupport).isTrue(); + } + + @DisplayName("GET 이외의 요청은 처리하지 않는다.") + @Test + void isNotSupport() { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("POST /js/test.js HTTP/1.1", headers, null); + + ResourceHandler handler = ResourceHandler.getInstance(); + + assertThat(handler.isSupport(request.getRequestLine())).isFalse(); + } + + @DisplayName("정적 리소스에 대한 요청을 처리한다.") + @Test + void handle() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("GET /index.html HTTP/1.1", headers, null); + + ResourceHandler handler = ResourceHandler.getInstance(); + + HttpResponse response = handler.handle(request); + assertThat(handler.isSupport(request.getRequestLine())).isTrue(); + assertThat(response) + .extracting("startLine") + .extracting("statusCode") + .isEqualTo(HttpStatusCode.OK); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/handler/RootPageHandlerTest.java b/tomcat/src/test/java/com/techcourse/web/handler/RootPageHandlerTest.java new file mode 100644 index 0000000000..30f44b9972 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/handler/RootPageHandlerTest.java @@ -0,0 +1,62 @@ +package com.techcourse.web.handler; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.util.List; + +import org.apache.coyote.http11.http.request.HttpRequest; +import org.apache.coyote.http11.http.request.HttpRequestLine; +import org.apache.coyote.http11.http.request.HttpRequestUrl; +import org.apache.coyote.http11.http.response.HttpResponse; +import org.apache.coyote.http11.http.response.HttpStatusCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RootPageHandlerTest { + + @DisplayName("/ 경로에 대한 요청을 처리할 수 있다.") + @Test + void isSupport() { + HttpRequestUrl requestUrl = HttpRequestUrl.from("/"); + HttpRequestLine requestLine = new HttpRequestLine("GET", requestUrl, "HTTP/1.1"); + RootPageHandler rootPageHandler = RootPageHandler.getInstance(); + + boolean isSupport = rootPageHandler.isSupport(requestLine); + + assertThat(isSupport).isTrue(); + } + + @DisplayName("GET 이외의 요청은 처리하지 않는다.") + @Test + void isNotSupport() { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("POST / HTTP/1.1", headers, null); + + RootPageHandler rootPageHandler = RootPageHandler.getInstance(); + + // then + assertThat(rootPageHandler.isSupport(request.getRequestLine())).isFalse(); + } + + @DisplayName("/ 요청을 처리한다.") + @Test + void handle() throws IOException { + List headers = List.of("Host: example.com", "Accept: text/html"); + HttpRequest request = new HttpRequest("GET / HTTP/1.1", headers, null); + + RootPageHandler rootPageHandler = RootPageHandler.getInstance(); + + // then + HttpResponse response = rootPageHandler.handle(request); + assertThat(rootPageHandler.isSupport(request.getRequestLine())).isTrue(); + assertThat(response) + .extracting("startLine") + .extracting("statusCode") + .isEqualTo(HttpStatusCode.OK); + assertThat(response) + .extracting("body") + .extracting("content") + .isEqualTo("Hello world!".getBytes()); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/util/FormUrlEncodedParserTest.java b/tomcat/src/test/java/com/techcourse/web/util/FormUrlEncodedParserTest.java new file mode 100644 index 0000000000..1487776ecf --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/util/FormUrlEncodedParserTest.java @@ -0,0 +1,43 @@ +package com.techcourse.web.util; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FormUrlEncodedParserTest { + + @DisplayName("Form Url Encoded 문자열을 파싱한다.") + @Test + void parse() { + String body = "name=abc&password=1234"; + + Map result = FormUrlEncodedParser.parse(body); + + assertThat(result).containsEntry("name", "abc").containsEntry("password", "1234"); + } + + @DisplayName("입력된 문자열이 없는 경우 예외를 던진다.") + @Test + void parse_WithEmptyInput() { + String body = ""; + + assertThatThrownBy(() -> FormUrlEncodedParser.parse(body)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Body is empty"); + } + + @DisplayName("Key만 존재하면 빈 문자열로 Value를 반환한다.") + @Test + void parse_WithEmptyValue() { + String body = "name=&password=1234"; + + Map result = FormUrlEncodedParser.parse(body); + + assertThat(result).containsEntry("name", "").containsEntry("password", "1234"); + } +} diff --git a/tomcat/src/test/java/com/techcourse/web/util/ResourceLoaderTest.java b/tomcat/src/test/java/com/techcourse/web/util/ResourceLoaderTest.java new file mode 100644 index 0000000000..f36d1cc9a2 --- /dev/null +++ b/tomcat/src/test/java/com/techcourse/web/util/ResourceLoaderTest.java @@ -0,0 +1,40 @@ +package com.techcourse.web.util; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.coyote.http11.http.response.HttpResponseBody; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResourceLoaderTest { + + @DisplayName("정적 리소스를 로드한다.") + @Test + void loadResource() throws IOException { + ResourceLoader resourceLoader = ResourceLoader.getInstance(); + String filePath = "/index.html"; + + HttpResponseBody responseBody = resourceLoader.loadResource(filePath); + + byte[] expected = Files.readAllBytes(Path.of(getClass().getResource("/static" + filePath).getPath())); + assertThat(responseBody).satisfies(body -> { + assertThat(body.getContentType()).isEqualTo("text/html"); + assertThat(body.getContent()).isEqualTo(expected); + }); + } + + @DisplayName("파일이 존재하지 않을 경우 예외를 던진다.") + @Test + void loadResourceWithNonExistFile() { + ResourceLoader resourceLoader = ResourceLoader.getInstance(); + String filePath = "/non-exist.html"; + + assertThatThrownBy(() -> resourceLoader.loadResource(filePath)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Resource not found. resource: " + "/static" + filePath); + } +} 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..ab4ac6e227 100644 --- a/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/org/apache/coyote/http11/Http11ProcessorTest.java @@ -1,14 +1,15 @@ package org.apache.coyote.http11; -import org.junit.jupiter.api.Test; -import support.StubSocket; +import static org.assertj.core.api.Assertions.*; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; + +import support.StubSocket; class Http11ProcessorTest { diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestMessageReaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestMessageReaderTest.java new file mode 100644 index 0000000000..c70cbaccec --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestMessageReaderTest.java @@ -0,0 +1,95 @@ +package org.apache.coyote.http11; + +import static org.assertj.core.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.coyote.http11.http.request.HttpMethod; +import org.apache.coyote.http11.http.request.HttpRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpRequestMessageReaderTest { + + @DisplayName("Http 요청 메시지를 읽는다.") + @Test + void read() throws IOException { + String body = """ + { + "name": "John Doe", + "age": 30, + "email": "john.doe@example.com", + "isActive": true + }"""; + + String httpRequest = String.join("\r\n", + "POST /api/endpoint?hi=hello&bangga= HTTP/1.1 ", + "Host: example.com ", + "Content-Length: " + body.length(), + "Content-Type: application/json ", + "Accept: application/json, text/plain, */* ", + "", + body); + + InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); + HttpRequest request = HttpRequestMessageReader.read(inputStream); + + System.out.println(request.getRequestBody()); + assertThat(request.getRequestLine()).satisfies( + requestLine -> { + assertThat(requestLine.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(requestLine.getRequestPath()).isEqualTo("/api/endpoint"); + assertThat(requestLine.getHttpVersion()).isEqualTo("HTTP/1.1"); + } + ); + assertThat(request.getHeaders()).satisfies( + headers -> { + assertThat(headers.getValue("Host")).containsOnly("example.com"); + assertThat(headers.getValue("Content-Length")).containsOnly(String.valueOf(body.length())); + assertThat(headers.getValue("Content-Type")).containsOnly("application/json"); + assertThat(headers.getValue("Accept")).containsExactly("application/json", "text/plain", "*/*"); + } + ); + assertThat(request.getRequestBody()).contains("name\": \"John Doe", "age\": 30", + "email\": \"john.doe@example.com", + "isActive\": true"); + } + + @DisplayName("Content-Length가 0인 경우 빈 body를 반환한다.") + @Test + void readEmptyBody() throws IOException { + String httpRequest = String.join("\r\n", + "GET /api/endpoint?hi=hello&bangga= HTTP/1.1 ", + "Host: example.com ", + "Content-Length: 0", + "Content-Type: application/json ", + "Accept: application/json, text/plain, */* ", + ""); + + InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); + HttpRequest request = HttpRequestMessageReader.read(inputStream); + + assertThat(request.getRequestBody()).isEmpty(); + } + + @DisplayName("Content-Length와 실제 body의 길이가 다른 경우 예외를 던진다.") + @Test + void readMismatchContentLength() { + String body = "Hello world!"; + String httpRequest = String.join("\r\n", + "POST /api/endpoint?hi=hello&bangga= HTTP/1.1 ", + "Host: example.com ", + "Content-Length: " + (body.length() + 1), + "Content-Type: application/json ", + "Accept: application/json, text/plain, */* ", + "", + body); + + InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); + assertThatThrownBy(() -> HttpRequestMessageReader.read(inputStream)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Content-Length mismatch"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestReaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestReaderTest.java deleted file mode 100644 index 71ef184688..0000000000 --- a/tomcat/src/test/java/org/apache/coyote/http11/HttpRequestReaderTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.apache.coyote.http11; - -import static org.assertj.core.api.Assertions.*; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class HttpRequestReaderTest { - - @DisplayName("Http 요청 메시지를 읽는다.") - @Test - void read() throws IOException { - String httpRequest = String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); - - InputStream inputStream = new ByteArrayInputStream(httpRequest.getBytes()); - HttpRequest request = HttpRequestReader.read(inputStream); - - assertThat(request).satisfies(r -> { - assertThat(r.getMethod()).isEqualTo("GET"); - assertThat(r.getRequestPath()).isEqualTo("/index.html"); - assertThat(r.getProtocol()).isEqualTo("HTTP/1.1"); - assertThat(r.getHeaders()).satisfies(headers -> { - assertThat(headers).containsEntry("Host", "localhost:8080"); - assertThat(headers).containsEntry("Connection", "keep-alive"); - }); - }); - } -} \ No newline at end of file