From 29421f333d45e2f890b0d25b1633d37f46688f18 Mon Sep 17 00:00:00 2001 From: LeeGeonHo Date: Mon, 11 Sep 2023 15:19:17 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=86=B0=EC=BA=A3=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EA=B8=B0=201,2=EB=8B=A8=EA=B3=84]=20=ED=98=B8?= =?UTF-8?q?=EC=9D=B4(=EC=9D=B4=EA=B1=B4=ED=98=B8)=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20=20(#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- study/src/test/java/study/FileTest.java | 51 ++++--- study/src/test/java/study/IOStreamTest.java | 40 ++++-- .../java/org/apache/catalina/Session.java | 31 +++++ .../org/apache/catalina/SessionManager.java | 20 +++ .../org/apache/coyote/http11/ContentType.java | 37 +++++ .../org/apache/coyote/http11/Controller.java | 9 ++ .../coyote/http11/ControllerAdapter.java | 37 +++++ .../org/apache/coyote/http11/Headers.java | 55 ++++++++ .../apache/coyote/http11/Http11Processor.java | 41 +++--- .../org/apache/coyote/http11/HttpMethod.java | 12 ++ .../org/apache/coyote/http11/StatusCode.java | 19 +++ .../org/apache/coyote/http11/ViewLoader.java | 42 ++++++ .../http11/controller/ErrorController.java | 13 ++ .../http11/controller/LoginController.java | 84 ++++++++++++ .../http11/controller/RegisterController.java | 56 ++++++++ .../http11/controller/StaticController.java | 30 ++++ .../coyote/http11/request/HttpCookie.java | 31 +++++ .../coyote/http11/request/HttpRequest.java | 128 ++++++++++++++++++ .../coyote/http11/request/QueryString.java | 31 +++++ .../coyote/http11/request/RequestBody.java | 43 ++++++ .../coyote/http11/response/HttpResponse.java | 98 ++++++++++++++ tomcat/src/main/resources/static/favicon.ico | 0 tomcat/src/main/resources/static/login.html | 2 +- .../coyote/http11/ControllerAdapterTest.java | 63 +++++++++ .../apache/coyote/http11/ViewLoaderTest.java | 50 +++++++ 25 files changed, 972 insertions(+), 51 deletions(-) create mode 100644 tomcat/src/main/java/org/apache/catalina/Session.java create mode 100644 tomcat/src/main/java/org/apache/catalina/SessionManager.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ContentType.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/Controller.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ControllerAdapter.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/Headers.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/StatusCode.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/ViewLoader.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/controller/ErrorController.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/controller/StaticController.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java create mode 100644 tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java create mode 100644 tomcat/src/main/resources/static/favicon.ico create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/ControllerAdapterTest.java create mode 100644 tomcat/src/test/java/org/apache/coyote/http11/ViewLoaderTest.java diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..5c562e9938 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,60 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.nio.file.Paths; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + final Path path = getFilePath(fileName); + + assertThat(path.toString()).endsWith(fileName); + } - assertThat(actual).endsWith(fileName); + private Path getFilePath(String fileName) { + final String currentPath = Paths.get("").toAbsolutePath().toString(); + final Path path = Paths.get(currentPath + "/src/test/resources/" + fileName); + final File file = path.toFile(); + if (!file.exists()) { + System.out.println("파일이 존재하지 않습니다."); + return null; + } + return path; } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final Path path = null; + final Path path = getFilePath(fileName); - // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..5f622c974c 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -50,10 +50,9 @@ class OutputStream_학습_테스트 { final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); /** - * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ - + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -74,10 +73,10 @@ class OutputStream_학습_테스트 { final OutputStream outputStream = mock(BufferedOutputStream.class); /** - * todo * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -92,11 +91,13 @@ class OutputStream_학습_테스트 { final OutputStream outputStream = mock(OutputStream.class); /** - * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream){ + } + verify(outputStream, atLeastOnce()).close(); } } @@ -125,10 +126,16 @@ class InputStream_학습_테스트 { final InputStream inputStream = new ByteArrayInputStream(bytes); /** - * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + int n; + String actual = ""; + try(final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + while((n = inputStream.read()) != -1) { + outputStream.write(n); + } + actual = outputStream.toString(); + } assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -144,10 +151,11 @@ class InputStream_학습_테스트 { final InputStream inputStream = mock(InputStream.class); /** - * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream){ + } verify(inputStream, atLeastOnce()).close(); } @@ -166,15 +174,15 @@ class FilterStream_학습_테스트 { /** * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? -> 8192 (8KiB) */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,7 +205,7 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", @@ -205,8 +213,16 @@ class InputStreamReader_학습_테스트 { ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + final StringBuilder actual = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + actual.append(line + "\r\n"); + } + bufferedReader.close(); + inputStream.close(); assertThat(actual).hasToString(emoji); } } diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java new file mode 100644 index 0000000000..3a5ad1f4b0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Session.java @@ -0,0 +1,31 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session() { + this(UUID.randomUUID().toString()); + } + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(final String name) { + return values.getOrDefault(name, null); + } + + public void setAttribute(final String name, final Object value) { + values.put(name, value); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java new file mode 100644 index 0000000000..09bbffb7c8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -0,0 +1,20 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private SessionManager() { + } + + private static final Map SESSIONS = new HashMap<>(); + + public static void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public static Session findSession(final String id) { + return SESSIONS.getOrDefault(id, null); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java new file mode 100644 index 0000000000..b1b19cc467 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11; + +public enum ContentType { + + TEXT_HTML("text/html"), + TEXT_CSS("text/css"), + APPLICATION_JS("application/js"), + WILD_CARD("*/*"); + + private static final String CSS = "css"; + private static final String HTML = "html"; + private static final String JS = "js"; + private static final String ENCODING = ";charset=utf-8"; + + private String value; + + ContentType(final String value) { + this.value = value; + } + + public String getValue() { + return value + ENCODING; + } + + public static ContentType from(String extension) { + if (extension.equalsIgnoreCase(CSS)) { + return TEXT_CSS; + } + if (extension.equalsIgnoreCase(HTML)) { + return TEXT_HTML; + } + if (extension.equalsIgnoreCase(JS)) { + return APPLICATION_JS; + } + return WILD_CARD; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/Controller.java new file mode 100644 index 0000000000..42d0cefb9a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Controller.java @@ -0,0 +1,9 @@ +package org.apache.coyote.http11; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public interface Controller { + + HttpResponse handle(HttpRequest request); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ControllerAdapter.java b/tomcat/src/main/java/org/apache/coyote/http11/ControllerAdapter.java new file mode 100644 index 0000000000..33fc831967 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ControllerAdapter.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.coyote.http11.controller.ErrorController; +import org.apache.coyote.http11.controller.LoginController; +import org.apache.coyote.http11.controller.RegisterController; +import org.apache.coyote.http11.controller.StaticController; +import org.apache.coyote.http11.request.HttpRequest; + +public class ControllerAdapter { + + private static final Map map = new ConcurrentHashMap<>(); + private static final String STATIC_CONTROLLER = "staticController"; + private static final String ERROR_CONTROLLER = "errorController"; + + public ControllerAdapter() { + init(); + } + + private void init() { + map.put(STATIC_CONTROLLER, new StaticController()); + map.put(ERROR_CONTROLLER, new ErrorController()); + map.put("/login", new LoginController()); + map.put("/register", new RegisterController()); + } + + public Controller findController(HttpRequest httpRequest) { + if (httpRequest.isStaticRequest()) { + return map.get(STATIC_CONTROLLER); + } + if (map.containsKey(httpRequest.getUri())) { + return map.get(httpRequest.getUri()); + } + return map.get(ERROR_CONTROLLER); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Headers.java b/tomcat/src/main/java/org/apache/coyote/http11/Headers.java new file mode 100644 index 0000000000..c8029d4f31 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Headers.java @@ -0,0 +1,55 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.coyote.http11.request.HttpCookie; + +public class Headers { + + private static final String HEADER_DELIMITER = ":"; + private Map values; + private HttpCookie cookie; + + private Headers(Map values, HttpCookie cookie) { + this.values = values; + this.cookie = cookie; + } + + public static Headers from(final BufferedReader bufferedReader) throws IOException { + String line = ""; + final HashMap map = new HashMap<>(); + HttpCookie httpCookie = null; + + while (!(line = bufferedReader.readLine()).isBlank()) { + final String[] headerInfo = line.split(HEADER_DELIMITER); + final String headerName = headerInfo[0]; + final String value = headerInfo[1].trim(); + + if (headerName.equals("Cookie")) { + httpCookie = new HttpCookie(value); + } + map.put(headerName, value); + } + + return new Headers(map, httpCookie); + } + + public boolean containsHeader(final String header) { + return values.containsKey(header); + } + + public String get(final String header) { + return values.get(header); + } + + public boolean hasCookie() { + return Objects.nonNull(cookie); + } + + public HttpCookie getCookie() { + return cookie; + } +} 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 7f1b2c7e96..5ba4904f2b 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,16 +1,21 @@ package org.apache.coyote.http11; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.nio.charset.StandardCharsets; import nextstep.jwp.exception.UncheckedServletException; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final ControllerAdapter controllerAdapter = new ControllerAdapter(); private final Socket connection; @@ -27,21 +32,25 @@ public void run() { @Override public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - - final var responseBody = "Hello world!"; - - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); - - outputStream.write(response.getBytes()); + final var outputStream = connection.getOutputStream(); + final BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + + final HttpRequest request = new HttpRequest(bufferedReader); + final Controller controller = controllerAdapter.findController(request); + final HttpResponse httpResponse = getResponse(request, controller); + outputStream.write(httpResponse.toBytes()); outputStream.flush(); - } catch (IOException | UncheckedServletException e) { + } catch (IOException | UncheckedServletException | IllegalArgumentException e) { log.error(e.getMessage(), e); } } + + private HttpResponse getResponse(final HttpRequest request, final Controller controller) { + try { + return controller.handle(request); + } catch (IllegalArgumentException e) { + return HttpResponse.toNotFound(); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java new file mode 100644 index 0000000000..9ea3184e82 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java @@ -0,0 +1,12 @@ +package org.apache.coyote.http11; + +public enum HttpMethod { + POST, + GET, + PUT, + DELETE; + + public boolean isGet() { + return this.equals(GET); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/StatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/StatusCode.java new file mode 100644 index 0000000000..452c1d9c33 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/StatusCode.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11; + +public enum StatusCode { + OK("200 OK"), + CREATED("201 CREATED"), + FOUND("302 FOUND"), + UNAUTHORIZED("401 UNAUTHORIZED"), + NOT_FOUND("404 NOT FOUND"); + + private String value; + + StatusCode(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ViewLoader.java b/tomcat/src/main/java/org/apache/coyote/http11/ViewLoader.java new file mode 100644 index 0000000000..19f4355549 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ViewLoader.java @@ -0,0 +1,42 @@ +package org.apache.coyote.http11; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.Objects; + +public class ViewLoader { + + private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + private static final String STATIC_DIRECTORY = "static"; + + private ViewLoader() { + } + + public static String from(String viewName) { + URL resource = classLoader.getResource(STATIC_DIRECTORY + viewName); + + if (Objects.isNull(resource)) { + return toNotFound(); + } + final File file = new File(resource.getFile()); + try{ + return new String(Files.readAllBytes(file.toPath())); + } catch (IOException e){ + return null; + } + } + + public static String toIndex() { + return from("/index.html"); + } + + public static String toNotFound() { + return from("/404.html"); + } + + public static String toUnauthorized() { + return from("/401.html"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/ErrorController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/ErrorController.java new file mode 100644 index 0000000000..3dcaf40e2a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/ErrorController.java @@ -0,0 +1,13 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.Controller; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public class ErrorController implements Controller { + + @Override + public HttpResponse handle(final HttpRequest request) { + return HttpResponse.toNotFound(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java new file mode 100644 index 0000000000..7dc7f5041e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java @@ -0,0 +1,84 @@ +package org.apache.coyote.http11.controller; + +import java.util.Objects; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.Session; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.Controller; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.StatusCode; +import org.apache.coyote.http11.ViewLoader; +import org.apache.coyote.http11.request.RequestBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginController implements Controller { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + private static final int LOGIN_PARAMETER_SIZE = 2; + + @Override + public HttpResponse handle(final HttpRequest request) { + if (request.isGetRequest()) { + return handleGetMethod(request); + } + return handlePostMethod(request); + } + + private HttpResponse handleGetMethod(final HttpRequest request) { + if (request.hasJSessionId() && Objects.nonNull(request.getSession(false))) { + final Session session = request.getSession(false); + final User user = (User) session.getAttribute("user"); + if (Objects.nonNull(user)) { + return HttpResponse.builder() + .statusCode(StatusCode.FOUND) + .contentType(ContentType.TEXT_HTML) + .responseBody(ViewLoader.toIndex()) + .redirect("/index.html") + .build(); + } + } + return HttpResponse.builder() + .statusCode(StatusCode.OK) + .contentType(ContentType.TEXT_HTML) + .responseBody(ViewLoader.from("/login.html")) + .build(); + } + + private HttpResponse handlePostMethod(final HttpRequest request) { + final RequestBody requestBody = request.getRequestBody(); + if (Objects.isNull(requestBody) || requestBody.size() != LOGIN_PARAMETER_SIZE) { + return HttpResponse.toUnauthorized(); + } + final String account = requestBody.get("account"); + final String password = requestBody.get("password"); + final User user = login(account, password); + + final Session session = request.getSession(true); + session.setAttribute("user", user); + + return HttpResponse.builder() + .statusCode(StatusCode.FOUND) + .contentType(ContentType.TEXT_HTML) + .responseBody(ViewLoader.toIndex()) + .addCookie(session.getId()) + .redirect("/index.html") + .build(); + } + + private User login(final String account, final String password) { + final User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 계정입니다.")); + validatePassword(user, password); + return user; + } + + private void validatePassword(final User user, final String password) { + if (user.checkPassword(password)) { + return; + } + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java new file mode 100644 index 0000000000..e4ddb3018a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/RegisterController.java @@ -0,0 +1,56 @@ +package org.apache.coyote.http11.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.Controller; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.StatusCode; +import org.apache.coyote.http11.ViewLoader; +import org.apache.coyote.http11.request.RequestBody; + +public class RegisterController implements Controller { + + @Override + public HttpResponse handle(final HttpRequest request) { + if (request.isGetRequest()) { + return handleGetMethod(); + } + return handlePostMethod(request); + } + + private HttpResponse handleGetMethod() { + return HttpResponse.builder() + .statusCode(StatusCode.OK) + .contentType(ContentType.TEXT_HTML) + .responseBody(ViewLoader.from("/register.html")) + .build(); + } + + private HttpResponse handlePostMethod(final HttpRequest request) { + if (request.hasRequestBody()) { + final RequestBody requestBody = request.getRequestBody(); + register(requestBody); + return HttpResponse.builder() + .statusCode(StatusCode.CREATED) + .contentType(ContentType.TEXT_HTML) + .responseBody(ViewLoader.toIndex()) + .build(); + } + return HttpResponse.toNotFound(); + } + + private void register(final RequestBody requestBody) { + try { + final String account = requestBody.get("account"); + final String password = requestBody.get("password"); + final String email = requestBody.get("email"); + + final User user = new User(account, password, email); + InMemoryUserRepository.save(user); + } catch (NullPointerException e) { + e.getStackTrace(); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticController.java new file mode 100644 index 0000000000..13768e4dbb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/StaticController.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.Controller; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.StatusCode; +import org.apache.coyote.http11.ViewLoader; + +public class StaticController implements Controller { + + private static final String INDEX_URI = "/"; + + @Override + public HttpResponse handle(HttpRequest request) { + if (request.getUri().equals(INDEX_URI)) { + return HttpResponse.builder() + .statusCode(StatusCode.OK) + .contentType(ContentType.TEXT_HTML) + .responseBody("Hello world!") + .build(); + } + + return HttpResponse.builder() + .statusCode(StatusCode.OK) + .contentType(ContentType.from(request.getExtension())) + .responseBody(ViewLoader.from(request.getUri())) + .build(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java new file mode 100644 index 0000000000..a8efc88502 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class HttpCookie { + + private static final String JSESSIONID = "JSESSIONID"; + private final Map cookies = new HashMap<>(); + + public HttpCookie(String requestCookie) { + final String[] cookies = requestCookie.replace(";", "").split(" "); + for (String cookie : cookies) { + final String[] cookieInfo = cookie.split("="); + final String key = cookieInfo[0]; + final String value = cookieInfo[1]; + this.cookies.put(key, value); + } + } + + public boolean hasJSessionId() { + return cookies.containsKey(JSESSIONID); + } + + public String getJsessionid() { + if (cookies.containsKey(JSESSIONID)) { + throw new IllegalArgumentException("Session이 존재하지 않습니다."); + } + return cookies.get(JSESSIONID); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..647e54d072 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,128 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Objects; +import org.apache.catalina.Session; +import org.apache.catalina.SessionManager; +import org.apache.coyote.http11.Headers; +import org.apache.coyote.http11.HttpMethod; + +public class HttpRequest { + + private static final String REQUEST_API_DELIMITER = " "; + private static final int HTTP_METHOD_INDEX = 0; + private static final int REQUEST_URI_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + private static final String QUERY_STRING_SYMBOL = "?"; + + private static final String DOT = "."; + private static final int START_LINE_SIZE = 3; + + private final HttpMethod method; + private final String uri; + private final String version; + private Headers headers; + private RequestBody requestBody; + private QueryString queryString; + + public HttpRequest(BufferedReader bufferedReader) throws IOException { + final String requestApi = bufferedReader.readLine(); + final String[] apiInfo = requestApi.split(REQUEST_API_DELIMITER); + + if (apiInfo.length != START_LINE_SIZE) { + throw new IllegalArgumentException("잘못된 http 요청 입니다."); + } + + this.method = HttpMethod.valueOf(apiInfo[HTTP_METHOD_INDEX]); + this.uri = apiInfo[REQUEST_URI_INDEX]; + this.version = apiInfo[HTTP_VERSION_INDEX]; + initHeaders(bufferedReader); + initRequestBody(bufferedReader); + initQueryString(); + } + + private void initHeaders(final BufferedReader bufferedReader) throws IOException { + headers = Headers.from(bufferedReader); + } + + private void initRequestBody(final BufferedReader bufferedReader) throws IOException { + if (headers.containsHeader("Content-Length")) { + int contentLength = Integer.parseInt(headers.get("Content-Length")); + requestBody = RequestBody.of(bufferedReader, contentLength); + } + } + + private void initQueryString() { + if (hasQueryString()) { + this.queryString = QueryString.of(uri); + } + } + + public boolean hasQueryString() { + return uri.contains(QUERY_STRING_SYMBOL); + } + + public String getUri() { + if (hasQueryString()) { + final int queryIndex = uri.indexOf(QUERY_STRING_SYMBOL); + return uri.substring(0, queryIndex); + } + return uri; + } + + public boolean isStaticRequest() { + return uri.contains(DOT) || uri.equals("/"); + } + + public String getExtension() { + final int dotIndex = uri.indexOf(DOT); + return uri.substring(dotIndex + 1); + } + + public QueryString getQueryString() { + return queryString; + } + + public boolean isGetRequest() { + return method.isGet(); + } + + public boolean hasRequestBody() { + return Objects.nonNull(requestBody); + } + + public RequestBody getRequestBody() { + return requestBody; + } + + public boolean hasCookie() { + return headers.hasCookie(); + } + + public boolean hasJSessionId() { + if (hasCookie()) { + final HttpCookie cookie = headers.getCookie(); + return cookie.hasJSessionId(); + } + return false; + } + + public Session getSession(boolean create) { + if (hasJSessionId()) { + final HttpCookie cookie = headers.getCookie(); + final String jsessionid = cookie.getJsessionid(); + Session session = SessionManager.findSession(jsessionid); + if (Objects.nonNull(session)) { + return session; + } + } + + if (!create) { + return null; + } + final Session session = new Session(); + SessionManager.add(session); + return session; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java new file mode 100644 index 0000000000..c124dd2aaa --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/QueryString.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class QueryString { + + private static final String QUERY_STRING_SYMBOL = "?"; + + private Map values; + + private QueryString(Map values) { + this.values = values; + } + + public static QueryString of(String uri) { + Map map = new HashMap<>(); + final int queryIndex = uri.indexOf(QUERY_STRING_SYMBOL); + final String[] queryParameters = uri.substring(queryIndex + 1).split("&"); + for (String queryParameter : queryParameters) { + final String[] queryKeyAndValue = queryParameter.split("="); + if (queryKeyAndValue.length != 2) { + throw new IllegalArgumentException("잘못된 Query String 입니다."); + } + final String key = queryKeyAndValue[0]; + final String value = queryKeyAndValue[1]; + map.put(key, value); + } + return new QueryString(map); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java new file mode 100644 index 0000000000..aa76213525 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class RequestBody { + + private static final String REQUEST_BODY_DELIMITER = "&"; + private static final String KEY_VALUE_DELIMITER = "="; + private Map values; + + private RequestBody(Map values) { + this.values = values; + } + + public static RequestBody of(final BufferedReader bufferedReader, final int contentLength) throws IOException { + final Map map = new HashMap<>(); + + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + String requestBodyBuffer = new String(buffer); + + final String[] requestBodies = requestBodyBuffer.split(REQUEST_BODY_DELIMITER); + for (String body : requestBodies) { + final String[] requestBodyInfo = body.split(KEY_VALUE_DELIMITER); + final String requestBodyName = requestBodyInfo[0]; + final String requestBodyValue = requestBodyInfo[1]; + map.put(requestBodyName, requestBodyValue); + } + + return new RequestBody(map); + } + + public int size() { + return values.size(); + } + + public String get(final String key) { + return values.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..c7c91edcb3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,98 @@ +package org.apache.coyote.http11.response; + +import java.util.HashMap; +import java.util.Map; +import org.apache.coyote.http11.ContentType; +import org.apache.coyote.http11.StatusCode; +import org.apache.coyote.http11.ViewLoader; + +public class HttpResponse { + + private static final String CRLF = "\r\n"; + private static final String SET_COOKIE = "Set-Cookie"; + private static final String HTTP_VERSION = "HTTP/1.1 "; + private static final String CONTENT_TYPE = "Content-Type"; + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String SP = " "; + private static final String HEADER_DELIMITER = ": "; + + private final StatusCode statusCode; + private final ContentType contentType; + private final String responseBody; + private final Map headers; + + private HttpResponse(final StatusCode statusCode, final ContentType contentType, final String responseBody, final Map headers) { + this.statusCode = statusCode; + this.contentType = contentType; + this.responseBody = responseBody; + this.headers = headers; + } + + public byte[] toBytes() { + String responseHeader = String.join(CRLF, + HTTP_VERSION + statusCode.getValue() + SP, + CONTENT_TYPE + HEADER_DELIMITER + contentType.getValue() + SP, + CONTENT_LENGTH + HEADER_DELIMITER + responseBody.getBytes().length + SP); + + for (Map.Entry entry : headers.entrySet()) { + String headerInfo = entry.getKey() + HEADER_DELIMITER + entry.getValue() + SP; + responseHeader = String.join(CRLF, responseHeader, headerInfo); + } + + return String.join(CRLF, responseHeader, "", responseBody).getBytes(); + } + + public static HttpResponse toNotFound() { + return new HttpResponse(StatusCode.NOT_FOUND, ContentType.TEXT_HTML, ViewLoader.toNotFound(), null); + } + + public static HttpResponse toUnauthorized() { + return new HttpResponse(StatusCode.UNAUTHORIZED, ContentType.TEXT_HTML, ViewLoader.toUnauthorized(), null); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private static final String LOCATION = "Location"; + private static final String JSESSIONID = "JSESSIONID="; + private StatusCode statusCode = StatusCode.OK; + private ContentType contentType = ContentType.WILD_CARD; + private String responseBody = ""; + private Map headers = new HashMap<>(); + + private Builder() { + } + + public Builder statusCode(final StatusCode statusCode) { + this.statusCode = statusCode; + return this; + } + + public Builder contentType(final ContentType contentType) { + this.contentType = contentType; + return this; + } + + public Builder responseBody(final String responseBody) { + this.responseBody = responseBody; + return this; + } + + public Builder redirect(final String redirectUrl) { + headers.put(LOCATION, redirectUrl); + return this; + } + + public Builder addCookie(String sessionId) { + headers.put(SET_COOKIE, JSESSIONID + sessionId); + return this; + } + + public HttpResponse build() { + return new HttpResponse(statusCode, contentType, responseBody, headers); + } + } +} diff --git a/tomcat/src/main/resources/static/favicon.ico b/tomcat/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 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/org/apache/coyote/http11/ControllerAdapterTest.java b/tomcat/src/test/java/org/apache/coyote/http11/ControllerAdapterTest.java new file mode 100644 index 0000000000..6b8390ee82 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/ControllerAdapterTest.java @@ -0,0 +1,63 @@ +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import org.apache.coyote.http11.controller.ErrorController; +import org.apache.coyote.http11.controller.LoginController; +import org.apache.coyote.http11.controller.RegisterController; +import org.apache.coyote.http11.controller.StaticController; +import org.apache.coyote.http11.request.HttpRequest; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ControllerAdapterTest { + + @DisplayName("Request에 해당하는 컨트롤러를 찾아온다.") + @Test + void findController() throws IOException { + //given + final ControllerAdapter controllerAdapter = new ControllerAdapter(); + + //when + final String index = "GET /index.html HTTP/1.1"; + final String login = "GET /login HTTP/1.1"; + final String register = "GET /register HTTP/1.1"; + final String error = "GET /sd HTTP/1.1"; + + final HttpRequest indexRequest = new HttpRequest(getBufferedReader(index)); + final HttpRequest loginRequest = new HttpRequest(getBufferedReader(login)); + final HttpRequest registerRequest = new HttpRequest(getBufferedReader(register)); + final HttpRequest errorRequest = new HttpRequest(getBufferedReader(error)); + + //then + SoftAssertions.assertSoftly( + soft -> { + soft.assertThat(controllerAdapter.findController(indexRequest)).isInstanceOf( + StaticController.class); + soft.assertThat(controllerAdapter.findController(loginRequest)).isInstanceOf( + LoginController.class); + soft.assertThat(controllerAdapter.findController(registerRequest)).isInstanceOf( + RegisterController.class); + soft.assertThat(controllerAdapter.findController(errorRequest)).isInstanceOf( + ErrorController.class); + } + ); + } + + private BufferedReader getBufferedReader(final String request) { + return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(request.getBytes()))) { + boolean fist = true; + @Override + public String readLine() { + if (fist) { + fist = false; + return request; + } + return ""; + } + }; + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/ViewLoaderTest.java b/tomcat/src/test/java/org/apache/coyote/http11/ViewLoaderTest.java new file mode 100644 index 0000000000..86a9738835 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/ViewLoaderTest.java @@ -0,0 +1,50 @@ +package org.apache.coyote.http11; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ViewLoaderTest { + + @DisplayName("view 이름에 맞는 파일을 읽고 반환한다.") + @Test + void from() { + //given + final String html = ViewLoader.from("/index.html"); + + //then + Assertions.assertThat(html).contains("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 대시보드\n" + + " \n" + + " \n" + + " "); + } + + @DisplayName("view에 맞는 파일이 없으면 404 페이지를 반환한다.") + @Test + void from_no_file() { + //given + final String html = ViewLoader.from("sdsdsdsd.html"); + + //then + Assertions.assertThat(html).contains("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 404 Error - SB Admin\n" + + " \n" + + " \n" + + " "); + } +}