diff --git a/README.md b/README.md
index b24f542e33..e9f4ac29a2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,17 @@
# 톰캣 구현하기
+
+## 기능 요구 사항
+
+### GET /index.html 응답하기 ✅
+
+### CSS 지원하기 ✅
+
+### Query String 파싱 ✅
+
+### HTTP Status Code 302 ✅
+
+### POST 방식으로 회원가입 ✅
+
+### Cookie에 JSESSIONID 값 저장하기 ✅
+
+### Session 구현하기 ✅
diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java
index e1b6cca042..b5e44f44f7 100644
--- a/study/src/test/java/study/FileTest.java
+++ b/study/src/test/java/study/FileTest.java
@@ -1,53 +1,52 @@
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.IOException;
+import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Collections;
import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import java.util.Objects;
+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 URL resource = getClass().getClassLoader().getResource(fileName);
+ final String actual = Objects.requireNonNull(resource).getFile();
- assertThat(actual).endsWith(fileName);
+ assertThat(actual).isNotNull()
+ .endsWith(fileName);
}
/**
* 파일 내용 읽기
- *
- * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
- * File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
+ *
+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
- void 파일의_내용을_읽는다() {
+ void 파일의_내용을_읽는다() throws IOException {
final String fileName = "nextstep.txt";
+ final URL resource = getClass().getClassLoader().getResource(fileName);
+ final Path path = Path.of(Objects.requireNonNull(resource).getFile());
- // todo
- final Path path = null;
-
- // 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..1fe72fe837 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -1,45 +1,51 @@
package study;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import java.io.*;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
-
/**
- * 자바는 스트림(Stream)으로부터 I/O를 사용한다.
- * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
- *
- * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
- * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
- * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
- * Stream은 데이터를 바이트로 읽고 쓴다.
- * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
- * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
+ * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
+ *
+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는
+ * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
+ *
+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을
+ * 처리할 수 있다.
*/
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
/**
* OutputStream 학습하기
- *
- * 자바의 기본 출력 클래스는 java.io.OutputStream이다.
- * OutputStream의 write(int b) 메서드는 기반 메서드이다.
+ *
+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다.
* public abstract void write(int b) throws IOException;
*/
@Nested
class OutputStream_학습_테스트 {
/**
- * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다.
- * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
- * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
- * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
- *
+ * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를
+ * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때
+ * 사용한다.
+ *
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* write(byte[] data)와 write(byte b[], int off, int len) 메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
@@ -53,7 +59,7 @@ class OutputStream_학습_테스트 {
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/
-
+ outputStream.write(bytes);
final String actual = outputStream.toString();
assertThat(actual).isEqualTo("nextstep");
@@ -61,13 +67,10 @@ class OutputStream_학습_테스트 {
}
/**
- * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
- * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
- * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
- * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
- * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
- * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
+ * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
+ *
+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은
+ * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
*/
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
@@ -78,14 +81,14 @@ class OutputStream_학습_테스트 {
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/
+ outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
/**
- * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
- * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
+ * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
@@ -96,6 +99,9 @@ class OutputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try (outputStream) {
+
+ }
verify(outputStream, atLeastOnce()).close();
}
@@ -103,20 +109,18 @@ class OutputStream_학습_테스트 {
/**
* InputStream 학습하기
- *
- * 자바의 기본 입력 클래스는 java.io.InputStream이다.
- * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
- * InputStream의 read() 메서드는 기반 메서드이다.
+ *
+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반
+ * 메서드이다.
* public abstract int read() throws IOException;
- *
+ *
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
class InputStream_학습_테스트 {
/**
- * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다.
- * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
+ * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
* 그리고 Stream 끝에 도달하면 -1을 반환한다.
*/
@Test
@@ -128,7 +132,8 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
- final String actual = "";
+
+ final String actual = new String(inputStream.readAllBytes());
assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
@@ -136,8 +141,7 @@ class InputStream_학습_테스트 {
}
/**
- * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
- * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
+ * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
@@ -148,6 +152,7 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ inputStream.close();
verify(inputStream, atLeastOnce()).close();
}
@@ -155,59 +160,63 @@ class InputStream_학습_테스트 {
/**
* FilterStream 학습하기
- *
- * 필터는 필터 스트림, reader, writer로 나뉜다.
- * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
- * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
+ *
+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된
+ * 텍스트를 처리하는 데 사용된다.
*/
@Nested
class FilterStream_학습_테스트 {
/**
- * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다.
- * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다.
- * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
+ * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지
+ * 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@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, text.getBytes().length);
- final byte[] actual = new byte[0];
+ final byte[] actual = bufferedInputStream.readAllBytes();
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
+ inputStream.close();
+ bufferedInputStream.close();
}
}
/**
- * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다.
- * 문자열이 아닌 바이트 단위로 처리하려니 불편하다.
- * 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다.
- * reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다.
- * 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다.
+ * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader,
+ * writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다.
*/
@Nested
class InputStreamReader_학습_테스트 {
/**
- * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다.
- * 읽어온 문자(char)를 문자열(String)로 처리하자.
- * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
+ * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면
+ * readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
*/
@Test
- void BufferedReader를_사용하여_문자열을_읽어온다() {
+ void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException {
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
-
+ final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
final StringBuilder actual = new StringBuilder();
+ String line;
+
+ while ((line = br.readLine()) != null) {
+ actual.append(line);
+ actual.append("\r\n");
+ }
assertThat(actual).hasToString(emoji);
+ inputStream.close();
+ br.close();
}
}
}
diff --git a/tomcat/src/main/java/nextstep/jwp/application/RegisterService.java b/tomcat/src/main/java/nextstep/jwp/application/RegisterService.java
new file mode 100644
index 0000000000..f386a4e5d5
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/jwp/application/RegisterService.java
@@ -0,0 +1,11 @@
+package nextstep.jwp.application;
+
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+
+public class RegisterService {
+
+ public void register(final String account, final String password, final String email) {
+ InMemoryUserRepository.save(new User(account, password, email));
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/jwp/application/UserService.java b/tomcat/src/main/java/nextstep/jwp/application/UserService.java
new file mode 100644
index 0000000000..69a4b85942
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/jwp/application/UserService.java
@@ -0,0 +1,16 @@
+package nextstep.jwp.application;
+
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+
+public class UserService {
+
+ public boolean validateLogin(final String account, final String password) {
+ return getUserByAccount(account).checkPassword(password);
+ }
+
+ public User getUserByAccount(final String account) {
+ return InMemoryUserRepository.findByAccount(account)
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/web/HelloController.java b/tomcat/src/main/java/nextstep/web/HelloController.java
new file mode 100644
index 0000000000..bd0c9f12f9
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/web/HelloController.java
@@ -0,0 +1,14 @@
+package nextstep.web;
+
+import org.apache.coyote.http11.mvc.AbstractController;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class HelloController extends AbstractController {
+
+ @Override
+ public ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.fromSimpleStringData("Hello world!");
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/web/IndexController.java b/tomcat/src/main/java/nextstep/web/IndexController.java
new file mode 100644
index 0000000000..aae62f4dbd
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/web/IndexController.java
@@ -0,0 +1,14 @@
+package nextstep.web;
+
+import org.apache.coyote.http11.mvc.AbstractController;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class IndexController extends AbstractController {
+
+ @Override
+ public ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.forwardTo("/index.html");
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/web/LoginController.java b/tomcat/src/main/java/nextstep/web/LoginController.java
new file mode 100644
index 0000000000..5a142f1894
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/web/LoginController.java
@@ -0,0 +1,56 @@
+package nextstep.web;
+
+import java.util.UUID;
+import nextstep.jwp.application.UserService;
+import nextstep.jwp.model.User;
+import org.apache.coyote.http11.common.HttpStatus;
+import org.apache.coyote.http11.common.Session;
+import org.apache.coyote.http11.common.SessionManager;
+import org.apache.coyote.http11.mvc.AbstractController;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoginController extends AbstractController {
+
+ private static final Logger log = LoggerFactory.getLogger(LoginController.class);
+
+ private final UserService userService = new UserService();
+
+ @Override
+ public ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ final Session session = SessionManager.findSession(request.getCookie("JSESSIONID"));
+ if (session != null) {
+ final User user = (User) session.getAttribute("user");
+ log.info("already login: {}", user);
+ return ResponseEntity.redirectTo("/index.html");
+ }
+
+ return ResponseEntity.forwardTo("/login.html");
+ }
+
+ @Override
+ public ResponseEntity handlePostRequest(final HttpRequest request, final HttpResponse response) {
+ final String account = request.getPayloadValue("account");
+ final String password = request.getPayloadValue("password");
+ if (userService.validateLogin(account, password)) {
+ return successLogin(response, account);
+ }
+
+ return ResponseEntity.forwardTo(HttpStatus.UNAUTHORIZED, "/401.html");
+ }
+
+ private ResponseEntity successLogin(final HttpResponse response, final String account) {
+ final User user = userService.getUserByAccount(account);
+ log.info("login success: {}", user);
+
+ final String uuid = UUID.randomUUID().toString();
+ response.setCookie("JSESSIONID", uuid);
+ final Session session = new Session(uuid);
+ session.setAttribute("user", user);
+ SessionManager.add(session);
+ return ResponseEntity.redirectTo("/index.html");
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/web/RegisterController.java b/tomcat/src/main/java/nextstep/web/RegisterController.java
new file mode 100644
index 0000000000..1ee2df9aa1
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/web/RegisterController.java
@@ -0,0 +1,28 @@
+package nextstep.web;
+
+import nextstep.jwp.application.RegisterService;
+import org.apache.coyote.http11.mvc.AbstractController;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class RegisterController extends AbstractController {
+
+ private final RegisterService registerService = new RegisterService();
+
+ @Override
+ public ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.forwardTo("/register.html");
+ }
+
+ @Override
+ public ResponseEntity handlePostRequest(final HttpRequest request, final HttpResponse response) {
+ final String account = request.getPayloadValue("account");
+ final String password = request.getPayloadValue("password");
+ final String email = request.getPayloadValue("email");
+
+ registerService.register(account, password, email);
+
+ return ResponseEntity.redirectTo("/index.html");
+ }
+}
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..e2a7980683 100644
--- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
@@ -1,13 +1,18 @@
package org.apache.coyote.http11;
+import java.io.IOException;
+import java.net.Socket;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
+import org.apache.coyote.http11.mvc.ControllerMapping;
+import org.apache.coyote.http11.mvc.FrontController;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.request.parser.HttpRequestMessageReader;
+import org.apache.coyote.http11.response.HttpResponse;
+import org.apache.coyote.http11.response.formatter.HttpResponseMessageWriter;
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);
@@ -29,17 +34,13 @@ public void process(final Socket connection) {
try (final var inputStream = connection.getInputStream();
final var outputStream = connection.getOutputStream()) {
- final var responseBody = "Hello world!";
+ final HttpRequest httpRequest = HttpRequestMessageReader.readHttpRequest(inputStream);
+ final HttpResponse httpResponse = new HttpResponse();
- 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);
+ FrontController frontController = new FrontController(new ControllerMapping()); //FIXME DI
+ frontController.handleHttpRequest(httpRequest, httpResponse);
- outputStream.write(response.getBytes());
- outputStream.flush();
+ HttpResponseMessageWriter.writeHttpResponse(httpResponse, outputStream);
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java
new file mode 100644
index 0000000000..41202e58cf
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpCookie.java
@@ -0,0 +1,16 @@
+package org.apache.coyote.http11.common;
+
+import java.util.Map;
+
+public class HttpCookie {
+
+ private final Map cookies;
+
+ public HttpCookie(final Map cookies) {
+ this.cookies = cookies;
+ }
+
+ public String getCookieValue(final String cookieKey) {
+ return cookies.get(cookieKey);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java
new file mode 100644
index 0000000000..8251f23831
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpHeaders.java
@@ -0,0 +1,33 @@
+package org.apache.coyote.http11.common;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class HttpHeaders {
+
+ private final Map headers;
+
+ private HttpHeaders(final Map headers) {
+ this.headers = headers;
+ }
+
+ public static HttpHeaders getInstance() {
+ return new HttpHeaders(new LinkedHashMap<>());
+ }
+
+ public static HttpHeaders from(final Map headers) {
+ return new HttpHeaders(headers);
+ }
+
+ public void addHeader(final String header, final String value) {
+ headers.put(header, value);
+ }
+
+ public String get(final String header) {
+ return headers.get(header);
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java
new file mode 100644
index 0000000000..fb520bd77e
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpMethod.java
@@ -0,0 +1,16 @@
+package org.apache.coyote.http11.common;
+
+import java.util.Arrays;
+
+public enum HttpMethod {
+ GET,
+ POST,
+ ;
+
+ public static HttpMethod from(final String method) {
+ return Arrays.stream(HttpMethod.values())
+ .filter(httpMethod -> httpMethod.name().equalsIgnoreCase(method))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 HTTP 메서드입니다."));
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java
new file mode 100644
index 0000000000..f4dc053417
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/HttpStatus.java
@@ -0,0 +1,22 @@
+package org.apache.coyote.http11.common;
+
+public enum HttpStatus {
+ OK(200),
+ MOVED_PERMANENTLY(301),
+ FOUND(302),
+ UNAUTHORIZED(401),
+ NOT_FOUND(404),
+ METHOD_NOT_ALLOWED(405),
+ INTERNAL_SERVER_ERROR(500),
+ ;
+
+ private final int statusCode;
+
+ HttpStatus(final int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentType.java
new file mode 100644
index 0000000000..1c383f5bb5
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/ResourceContentType.java
@@ -0,0 +1,34 @@
+package org.apache.coyote.http11.common;
+
+import java.util.Arrays;
+
+public enum ResourceContentType {
+ HTML("html", "text/html;charset=utf-8"),
+ CSS("css", "text/css"),
+ SCRIPT("js", "text/javascript"),
+ ICON("ico", "image/x-icon"),
+ SVG("svg", "image/svg+xml"),
+ ;
+
+ private static final String EXTENSION_FLAG = ".";
+
+ private final String extension;
+ private final String contentType;
+
+ ResourceContentType(final String extension, final String contentType) {
+ this.extension = extension;
+ this.contentType = contentType;
+ }
+
+ public static ResourceContentType from(final String requestURI) {
+ final String extensionType = requestURI.substring(requestURI.lastIndexOf(EXTENSION_FLAG) + 1);
+ return Arrays.stream(values())
+ .filter(value -> value.extension.equalsIgnoreCase(extensionType))
+ .findFirst()
+ .orElse(HTML);
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java
new file mode 100644
index 0000000000..edaf065376
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/Session.java
@@ -0,0 +1,34 @@
+package org.apache.coyote.http11.common;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Session {
+
+ private final String id;
+ private final Map values = new HashMap<>();
+
+ public Session(final String id) {
+ this.id = id;
+ }
+
+ public Object getAttribute(final String name) {
+ return values.get(name);
+ }
+
+ public void setAttribute(final String name, final Object value) {
+ values.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ values.remove(name);
+ }
+
+ public void invalidate() {
+ values.clear();
+ }
+
+ public String getId() {
+ return id;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java
new file mode 100644
index 0000000000..bb7912f0f7
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/common/SessionManager.java
@@ -0,0 +1,25 @@
+package org.apache.coyote.http11.common;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SessionManager {
+
+ private static final Map SESSIONS = new HashMap<>();
+
+ public static void add(final Session session) {
+ SESSIONS.put(session.getId(), session);
+ }
+
+ public static Session findSession(final String id) {
+ return SESSIONS.get(id);
+ }
+
+ public static void remove(final String id) {
+ SESSIONS.remove(id);
+ }
+
+ private SessionManager() {
+ }
+}
+
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/AbstractController.java
new file mode 100644
index 0000000000..4620f9de79
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/AbstractController.java
@@ -0,0 +1,32 @@
+package org.apache.coyote.http11.mvc;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+import org.apache.coyote.http11.common.HttpMethod;
+import org.apache.coyote.http11.common.HttpStatus;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public abstract class AbstractController implements Controller {
+
+ private final Map> supportedMethods = Map.of(
+ HttpMethod.GET, this::handleGetRequest,
+ HttpMethod.POST, this::handlePostRequest
+ );
+
+
+ @Override
+ public ResponseEntity handleRequest(final HttpRequest request, final HttpResponse response) {
+ final HttpMethod httpMethod = request.getHttpStartLine().getHttpRequestMethod();
+ return supportedMethods.get(httpMethod).apply(request, response);
+ }
+
+ protected ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.forwardTo(HttpStatus.METHOD_NOT_ALLOWED, "/405.html");
+ }
+
+ protected ResponseEntity handlePostRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.forwardTo(HttpStatus.METHOD_NOT_ALLOWED, "/405.html");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/Controller.java
new file mode 100644
index 0000000000..b25533cffb
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/Controller.java
@@ -0,0 +1,10 @@
+package org.apache.coyote.http11.mvc;
+
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public interface Controller {
+
+ ResponseEntity handleRequest(HttpRequest request, HttpResponse response);
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/ControllerMapping.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/ControllerMapping.java
new file mode 100644
index 0000000000..b88da66a2f
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/ControllerMapping.java
@@ -0,0 +1,32 @@
+package org.apache.coyote.http11.mvc;
+
+import java.util.Map;
+import nextstep.web.HelloController;
+import nextstep.web.IndexController;
+import nextstep.web.LoginController;
+import nextstep.web.RegisterController;
+import org.apache.coyote.http11.request.HttpRequestLine;
+
+public class ControllerMapping {
+ private static final String HTML_EXTENSION = ".html";
+
+ private final Map controllerMap = Map.of(
+ "/", new HelloController(),
+ "/index", new IndexController(),
+ "/login", new LoginController(),
+ "/register", new RegisterController()
+ );
+
+ public Controller findController(final HttpRequestLine requestStartLine) {
+ final String requestURI = removeHtmlExtension(requestStartLine.getRequestURI());
+ return controllerMap.getOrDefault(requestURI, new ForwardController(requestStartLine.getRequestURI()));
+ }
+
+ private String removeHtmlExtension(final String requestURI) {
+ if (requestURI.endsWith(HTML_EXTENSION)) {
+ return requestURI.substring(requestURI.lastIndexOf(HTML_EXTENSION));
+ }
+
+ return requestURI;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/ForwardController.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/ForwardController.java
new file mode 100644
index 0000000000..579c251c89
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/ForwardController.java
@@ -0,0 +1,19 @@
+package org.apache.coyote.http11.mvc;
+
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class ForwardController extends AbstractController {
+
+ private final String forwardPath;
+
+ public ForwardController(final String forwardPath) {
+ this.forwardPath = forwardPath;
+ }
+
+ @Override
+ protected ResponseEntity handleGetRequest(final HttpRequest request, final HttpResponse response) {
+ return ResponseEntity.forwardTo(forwardPath);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/FrontController.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/FrontController.java
new file mode 100644
index 0000000000..49456161fe
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/FrontController.java
@@ -0,0 +1,21 @@
+package org.apache.coyote.http11.mvc;
+
+import java.io.IOException;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class FrontController {
+
+ private final ControllerMapping controllerMapping;
+
+ public FrontController(final ControllerMapping controllerMapping) {
+ this.controllerMapping = controllerMapping;
+ }
+
+ public void handleHttpRequest(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
+ final Controller controller = controllerMapping.findController(httpRequest.getHttpStartLine());
+ final ResponseEntity response = controller.handleRequest(httpRequest, httpResponse);
+ httpResponse.updateByResponseEntity(response);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/ResponseEntity.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/ResponseEntity.java
new file mode 100644
index 0000000000..1b3e5c4fc0
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/ResponseEntity.java
@@ -0,0 +1,63 @@
+package org.apache.coyote.http11.mvc.view;
+
+import java.util.Map;
+import org.apache.coyote.http11.common.HttpStatus;
+
+public class ResponseEntity {
+
+ private final HttpStatus httpStatus;
+ private final View view;
+ private final Map headers;
+
+ public ResponseEntity(final HttpStatus httpStatus, final View view) {
+ this.httpStatus = httpStatus;
+ this.view = view;
+ this.headers = Map.of();
+ }
+
+ public ResponseEntity(
+ final HttpStatus httpStatus,
+ final View view,
+ final Map headers
+ ) {
+ this.httpStatus = httpStatus;
+ this.view = view;
+ this.headers = headers;
+ }
+
+ public static ResponseEntity fromSimpleStringData(final String body) {
+ return new ResponseEntity(HttpStatus.OK, new SimpleStringDataView(body));
+ }
+
+ public static ResponseEntity forwardTo(final String path) {
+ return ResponseEntity.forwardTo(HttpStatus.OK, path);
+ }
+
+ public static ResponseEntity forwardTo(final HttpStatus httpStatus, final String path) {
+ try {
+ return new ResponseEntity(httpStatus, StaticResourceView.of(path));
+ } catch (IllegalArgumentException e) {
+ return new ResponseEntity(HttpStatus.NOT_FOUND, StaticResourceView.of("/404.html"));
+ }
+ }
+
+ public static ResponseEntity redirectTo(final String path) {
+ return new ResponseEntity(
+ HttpStatus.FOUND,
+ StaticResourceView.of(path),
+ Map.of("Location", path)
+ );
+ }
+
+ public HttpStatus getHttpStatus() {
+ return httpStatus;
+ }
+
+ public View getView() {
+ return view;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/SimpleStringDataView.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/SimpleStringDataView.java
new file mode 100644
index 0000000000..a226e698b8
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/SimpleStringDataView.java
@@ -0,0 +1,22 @@
+package org.apache.coyote.http11.mvc.view;
+
+public class SimpleStringDataView implements View {
+
+ private static final String CONTENT_TYPE = "text/plain;charset=utf-8";
+
+ private final String data;
+
+ public SimpleStringDataView(final String data) {
+ this.data = data;
+ }
+
+ @Override
+ public String renderView() {
+ return data;
+ }
+
+ @Override
+ public String getContentType() {
+ return CONTENT_TYPE;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/StaticResourceView.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/StaticResourceView.java
new file mode 100644
index 0000000000..5e0786d25a
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/StaticResourceView.java
@@ -0,0 +1,39 @@
+package org.apache.coyote.http11.mvc.view;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.Optional;
+import org.apache.coyote.http11.common.ResourceContentType;
+
+public class StaticResourceView implements View {
+
+ private static final String RESOURCE_DIRECTORY = "static/";
+
+ private final URL viewPath;
+ private final String contentType;
+
+ public StaticResourceView(final URL viewPath, final String contentType) {
+ this.viewPath = viewPath;
+ this.contentType = contentType;
+ }
+
+ public static StaticResourceView of(final String viewName) {
+ final ClassLoader classLoader = StaticResourceView.class.getClassLoader();
+ final URL viewPath = Optional.ofNullable(classLoader.getResource(RESOURCE_DIRECTORY + viewName))
+ .orElseThrow(() -> new IllegalArgumentException("Resource not found: " + viewName));
+ final String contentType = ResourceContentType.from(viewName).getContentType();
+ return new StaticResourceView(viewPath, contentType);
+ }
+
+ @Override
+ public String renderView() throws IOException {
+ return new String(Files.readAllBytes(new File(viewPath.getFile()).toPath()));
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/View.java b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/View.java
new file mode 100644
index 0000000000..e061263f60
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/mvc/view/View.java
@@ -0,0 +1,10 @@
+package org.apache.coyote.http11.mvc.view;
+
+import java.io.IOException;
+
+public interface View {
+
+ String renderView() throws IOException;
+
+ String getContentType();
+}
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..dec796040a
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java
@@ -0,0 +1,74 @@
+package org.apache.coyote.http11.request;
+
+import java.util.Map;
+import org.apache.coyote.http11.common.HttpCookie;
+import org.apache.coyote.http11.common.HttpHeaders;
+
+public class HttpRequest {
+
+ private final HttpRequestLine httpRequestLine;
+ private final HttpHeaders httpRequestHeaders;
+ private final Map queryParams;
+ private final Map payload;
+ private final HttpCookie cookie;
+
+ public HttpRequest(
+ final HttpRequestLine httpRequestLine,
+ final HttpHeaders httpRequestHeaders,
+ final HttpCookie cookie,
+ final Map payload
+ ) {
+ this.httpRequestLine = httpRequestLine;
+ this.httpRequestHeaders = httpRequestHeaders;
+ this.queryParams = httpRequestLine.getQueryParams();
+ this.cookie = cookie;
+ this.payload = payload;
+ }
+
+ public static HttpRequest of(
+ final HttpRequestLine httpRequestLine,
+ final HttpHeaders headers,
+ final HttpCookie cookie
+ ) {
+ return new HttpRequest(
+ httpRequestLine,
+ headers,
+ cookie,
+ Map.of()
+ );
+ }
+
+ public static HttpRequest of(
+ final HttpRequestLine httpRequestLine,
+ final HttpHeaders headers,
+ final HttpCookie cookie,
+ final Map payload
+ ) {
+ return new HttpRequest(
+ httpRequestLine,
+ headers,
+ cookie,
+ payload
+ );
+ }
+
+ public HttpRequestLine getHttpStartLine() {
+ return httpRequestLine;
+ }
+
+ public String getHeader(final String header) {
+ return httpRequestHeaders.get(header);
+ }
+
+ public String getParam(final String parameter) {
+ return queryParams.get(parameter);
+ }
+
+ public String getPayloadValue(final String key) {
+ return payload.get(key);
+ }
+
+ public String getCookie(final String cookieKey) {
+ return cookie.getCookieValue(cookieKey);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java
new file mode 100644
index 0000000000..41347afe5b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestLine.java
@@ -0,0 +1,40 @@
+package org.apache.coyote.http11.request;
+
+import java.util.Map;
+import org.apache.coyote.http11.common.HttpMethod;
+
+public class HttpRequestLine {
+
+ private final HttpMethod httpMethod;
+ private final String requestURI;
+ private final String httpVersion;
+ private final Map queryParams;
+
+ public HttpRequestLine(
+ final HttpMethod httpMethod,
+ final String requestURI,
+ final String httpVersion,
+ final Map queryParams
+ ) {
+ this.httpMethod = httpMethod;
+ this.requestURI = requestURI;
+ this.httpVersion = httpVersion;
+ this.queryParams = queryParams;
+ }
+
+ public HttpMethod getHttpRequestMethod() {
+ return httpMethod;
+ }
+
+ public String getRequestURI() {
+ return requestURI;
+ }
+
+ public String getHttpVersion() {
+ return httpVersion;
+ }
+
+ public Map getQueryParams() {
+ return queryParams;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/parser/ContentTypePayloadParserMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/ContentTypePayloadParserMapper.java
new file mode 100644
index 0000000000..29f60cc3ab
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/ContentTypePayloadParserMapper.java
@@ -0,0 +1,28 @@
+package org.apache.coyote.http11.request.parser;
+
+import java.util.Arrays;
+
+public enum ContentTypePayloadParserMapper {
+ APPLICATION_X_WWW_FORM_URLENCODED("application/x-www-form-urlencoded", new FormUrlEncodedPayloadParser()),
+ ;
+
+ private final String contentType;
+ private final PayloadParser payloadParser;
+
+ ContentTypePayloadParserMapper(final String contentType, final PayloadParser payloadParser) {
+ this.contentType = contentType;
+ this.payloadParser = payloadParser;
+ }
+
+ public static ContentTypePayloadParserMapper from(final String requestContentType) {
+ return Arrays.stream(ContentTypePayloadParserMapper.values())
+ .filter(contentTypePayloadParserMapper -> contentTypePayloadParserMapper.contentType.equalsIgnoreCase(
+ requestContentType))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 Content-Type 입니다."));
+ }
+
+ public PayloadParser getPayloadParser() {
+ return payloadParser;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/parser/FormUrlEncodedPayloadParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/FormUrlEncodedPayloadParser.java
new file mode 100644
index 0000000000..0a45be78af
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/FormUrlEncodedPayloadParser.java
@@ -0,0 +1,18 @@
+package org.apache.coyote.http11.request.parser;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class FormUrlEncodedPayloadParser implements PayloadParser {
+
+ private static final String PAYLOAD_SEPARATOR = "&";
+ private static final String PAYLOAD_DELIMITER = "=";
+
+ @Override
+ public Map parse(final String body) {
+ return Arrays.stream(body.split(PAYLOAD_SEPARATOR))
+ .map(param -> param.split(PAYLOAD_DELIMITER))
+ .collect(Collectors.toMap(e -> e[0], e -> e[1]));
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/parser/HttpRequestMessageReader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/HttpRequestMessageReader.java
new file mode 100644
index 0000000000..62edf5b1cf
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/HttpRequestMessageReader.java
@@ -0,0 +1,150 @@
+package org.apache.coyote.http11.request.parser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.coyote.http11.common.HttpCookie;
+import org.apache.coyote.http11.common.HttpHeaders;
+import org.apache.coyote.http11.common.HttpMethod;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.request.HttpRequestLine;
+
+public class HttpRequestMessageReader {
+
+ private static final String 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 int START_LINE_TOKEN_SIZE = 3;
+ private static final String QUERY_PARAMETER_START_FLAG = "?";
+ private static final String HTTP_HEADER_VALUE_DELIMITER = ":";
+ private static final String PARAMETER_SEPARATOR = "&";
+ private static final String QUERY_PARAMETER_DELIMITER = "=";
+ private static final String COOKIE_SEPARATOR = "; ";
+ private static final String COOKIE_DELIMITER = "=";
+
+ private HttpRequestMessageReader() {
+ }
+
+ public static HttpRequest readHttpRequest(final InputStream inputStream) throws IOException {
+ try (final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+ final HttpRequestLine startLine = readStartLine(br.readLine());
+ final HttpHeaders httpHeaders = readHeaders(br);
+ final HttpCookie httpCookie = parseCookieFromHeaders(httpHeaders);
+ return httpRequestByMethod(startLine, httpHeaders, httpCookie, br);
+ }
+ }
+
+ private static HttpRequest httpRequestByMethod(
+ final HttpRequestLine startLine,
+ final HttpHeaders headers,
+ final HttpCookie cookie,
+ final BufferedReader br
+ ) throws IOException {
+ if (startLine.getHttpRequestMethod() == HttpMethod.POST) {
+ return httpRequestWithBody(startLine, headers, cookie, br);
+ }
+ return HttpRequest.of(startLine, headers, cookie);
+ }
+
+ public static HttpRequestLine readStartLine(final String requestLine) {
+ final String[] startLineTokens = requestLine.split(DELIMITER);
+ validateStartLineTokenSize(startLineTokens);
+ final HttpMethod httpMethod = HttpMethod.from(startLineTokens[HTTP_METHOD_INDEX]);
+ final String URIWithQueryStrings = startLineTokens[REQUEST_URI_INDEX];
+ final String requestURI = parseURI(URIWithQueryStrings);
+ final Map queryParams = parseQueryParams(URIWithQueryStrings);
+ final String httpVersion = startLineTokens[HTTP_VERSION_INDEX];
+ return new HttpRequestLine(httpMethod, requestURI, httpVersion, queryParams);
+ }
+
+ private static Map parseQueryParams(final String URIWithQueryStrings) {
+ if (URIWithQueryStrings.contains(QUERY_PARAMETER_START_FLAG)) {
+ final int index = URIWithQueryStrings.indexOf(QUERY_PARAMETER_START_FLAG);
+ final String params = URIWithQueryStrings.substring(index + 1);
+ return Arrays.stream(params.split(PARAMETER_SEPARATOR))
+ .map(param -> param.split(QUERY_PARAMETER_DELIMITER))
+ .collect(Collectors.toMap(e -> e[0], e -> e[1]));
+ }
+ return Map.of();
+ }
+
+ private static String parseURI(final String URIWithQueryStrings) {
+ if (URIWithQueryStrings.contains(QUERY_PARAMETER_START_FLAG)) {
+ final int index = URIWithQueryStrings.indexOf(QUERY_PARAMETER_START_FLAG);
+ return URIWithQueryStrings.substring(0, index);
+ }
+ return URIWithQueryStrings;
+ }
+
+ private static void validateStartLineTokenSize(final String[] lines) {
+ if (lines.length != START_LINE_TOKEN_SIZE) {
+ throw new IllegalArgumentException("시작 라인의 토큰은 3개여야 합니다.");
+ }
+ }
+
+ private static HttpHeaders readHeaders(final BufferedReader br) throws IOException {
+ final Map headers = new LinkedHashMap<>();
+ String readLine = br.readLine();
+ while (readLine != null && !readLine.isEmpty()) {
+ addHeaderByReadLine(readLine, headers);
+ readLine = br.readLine();
+ }
+
+ return HttpHeaders.from(headers);
+ }
+
+ private static void addHeaderByReadLine(final String readLine, final Map headers) {
+ if (readLine == null || readLine.isEmpty()) {
+ return;
+ }
+
+ if (readLine.contains(HTTP_HEADER_VALUE_DELIMITER)) {
+ final int index = readLine.indexOf(HTTP_HEADER_VALUE_DELIMITER);
+ final String headerKey = readLine.substring(0, index).strip();
+ final String headerValue = readLine.substring(index + 1).strip();
+ headers.put(headerKey, headerValue);
+ return;
+ }
+
+ throw new IllegalStateException("헤더는 key: value 형태여야 합니다.");
+ }
+
+ public static HttpCookie parseCookieFromHeaders(final HttpHeaders headers) {
+ final String cookieValues = headers.get("Cookie");
+ if (cookieValues == null) {
+ return new HttpCookie(Map.of());
+ }
+
+ final Map cookieKeyValues = Arrays.stream(cookieValues.split(COOKIE_SEPARATOR))
+ .map(param -> param.split(COOKIE_DELIMITER))
+ .collect(Collectors.toMap(e -> e[0], e -> e[1]));
+ return new HttpCookie(cookieKeyValues);
+ }
+
+ private static HttpRequest httpRequestWithBody(
+ final HttpRequestLine startLine,
+ final HttpHeaders headers,
+ final HttpCookie cookie,
+ final BufferedReader br
+ ) throws IOException {
+ final ContentTypePayloadParserMapper contentTypePayloadParserMapper =
+ ContentTypePayloadParserMapper.from(headers.get("Content-Type"));
+ final String body = readBody(br, Integer.parseInt(headers.get("Content-Length")));
+ final PayloadParser payloadParser = contentTypePayloadParserMapper.getPayloadParser();
+ final Map payload = payloadParser.parse(body);
+ return HttpRequest.of(startLine, headers, cookie, payload);
+ }
+
+ private static String readBody(final BufferedReader br, final int contentLength) throws IOException {
+ final char[] body = new char[contentLength];
+ br.read(body, 0, contentLength);
+ return String.copyValueOf(body);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/parser/PayloadParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/PayloadParser.java
new file mode 100644
index 0000000000..110b6c7bea
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/parser/PayloadParser.java
@@ -0,0 +1,8 @@
+package org.apache.coyote.http11.request.parser;
+
+import java.util.Map;
+
+public interface PayloadParser {
+
+ Map parse(final String body);
+}
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..5f9e387b3c
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java
@@ -0,0 +1,63 @@
+package org.apache.coyote.http11.response;
+
+import java.io.IOException;
+import org.apache.coyote.http11.common.HttpHeaders;
+import org.apache.coyote.http11.common.HttpStatus;
+import org.apache.coyote.http11.mvc.view.ResponseEntity;
+
+public class HttpResponse {
+
+ private static final String SUPPORTED_HTTP_VERSION = "HTTP/1.1";
+ private static final HttpResponseStatusLine DEFAULT_RESPONSE_STATUS =
+ HttpResponseStatusLine.of(SUPPORTED_HTTP_VERSION, HttpStatus.OK);
+ private static final String NO_CONTENT = "";
+
+ private final HttpHeaders httpHeaders = HttpHeaders.getInstance();
+ private HttpResponseStatusLine httpResponseStatusLine = DEFAULT_RESPONSE_STATUS;
+ private String body;
+
+ public void updateHttpResponseStatusLineByStatus(final HttpStatus httpStatus) {
+ this.httpResponseStatusLine = HttpResponseStatusLine.of(SUPPORTED_HTTP_VERSION, httpStatus);
+ }
+
+ public void updateByResponseEntity(final ResponseEntity response) throws IOException {
+ final HttpStatus httpStatus = response.getHttpStatus();
+ updateHttpResponseStatusLineByStatus(httpStatus);
+ response.getHeaders().forEach(this::setHeader);
+ if (httpStatus == HttpStatus.FOUND) {
+ noContentResponse();
+ return;
+ }
+ setHeader("Content-Type", response.getView().getContentType());
+ setBody(response.getView().renderView());
+ }
+
+ private void noContentResponse() {
+ this.body = NO_CONTENT;
+ }
+
+ public HttpResponseStatusLine getHttpResponseStatusLine() {
+ return httpResponseStatusLine;
+ }
+
+ public HttpHeaders getHttpResponseHeaders() {
+ return httpHeaders;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(final String body) {
+ httpHeaders.addHeader("Content-Length", String.valueOf(body.getBytes().length));
+ this.body = body;
+ }
+
+ public void setHeader(final String header, final String value) {
+ httpHeaders.addHeader(header, value);
+ }
+
+ public void setCookie(final String cookieKey, final String cookieValue) {
+ httpHeaders.addHeader("Set-Cookie", cookieKey + "=" + cookieValue);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatusLine.java
new file mode 100644
index 0000000000..d1c56ad2c9
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStatusLine.java
@@ -0,0 +1,26 @@
+package org.apache.coyote.http11.response;
+
+import org.apache.coyote.http11.common.HttpStatus;
+
+public class HttpResponseStatusLine {
+
+ private final String httpVersion;
+ private final HttpStatus httpStatus;
+
+ private HttpResponseStatusLine(final String httpVersion, final HttpStatus httpStatus) {
+ this.httpVersion = httpVersion;
+ this.httpStatus = httpStatus;
+ }
+
+ public static HttpResponseStatusLine of(final String httpVersion, final HttpStatus httpStatus) {
+ return new HttpResponseStatusLine(httpVersion, httpStatus);
+ }
+
+ public String getHttpVersion() {
+ return httpVersion;
+ }
+
+ public HttpStatus getHttpResponseStatus() {
+ return httpStatus;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/formatter/HttpResponseMessageWriter.java b/tomcat/src/main/java/org/apache/coyote/http11/response/formatter/HttpResponseMessageWriter.java
new file mode 100644
index 0000000000..f3884fffda
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/formatter/HttpResponseMessageWriter.java
@@ -0,0 +1,45 @@
+package org.apache.coyote.http11.response.formatter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.stream.Collectors;
+import org.apache.coyote.http11.common.HttpHeaders;
+import org.apache.coyote.http11.common.HttpStatus;
+import org.apache.coyote.http11.response.HttpResponse;
+import org.apache.coyote.http11.response.HttpResponseStatusLine;
+
+public class HttpResponseMessageWriter {
+
+ private HttpResponseMessageWriter() {
+ }
+
+ public static void writeHttpResponse(final HttpResponse httpResponse, final OutputStream outputStream)
+ throws IOException {
+ final HttpResponseStatusLine httpResponseStatusLine = httpResponse.getHttpResponseStatusLine();
+ final HttpHeaders httpHeaders = httpResponse.getHttpResponseHeaders();
+ final String responseBody = httpResponse.getBody();
+ final String responseMessage = String.join("\r\n",
+ parseResponseStatusLine(httpResponseStatusLine),
+ parseResponseHeaders(httpHeaders),
+ "",
+ responseBody);
+
+ outputStream.write(responseMessage.getBytes());
+ outputStream.flush();
+ }
+
+ private static String parseResponseStatusLine(final HttpResponseStatusLine httpResponseStatusLine) {
+ final HttpStatus httpStatus = httpResponseStatusLine.getHttpResponseStatus();
+ return String.join(" ",
+ httpResponseStatusLine.getHttpVersion(),
+ String.valueOf(httpStatus.getStatusCode()),
+ httpStatus.toString()
+ ) + " ";
+ }
+
+ private static String parseResponseHeaders(final HttpHeaders httpHeaders) {
+ return httpHeaders.getHeaders().entrySet().stream()
+ .map(entry -> String.join(": ", entry.getKey(), entry.getValue() + " "))
+ .collect(Collectors.joining("\r\n"));
+ }
+}
diff --git a/tomcat/src/main/resources/static/405.html b/tomcat/src/main/resources/static/405.html
new file mode 100644
index 0000000000..f1cd380b29
--- /dev/null
+++ b/tomcat/src/main/resources/static/405.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+ 404 Error - SB Admin
+
+
+
+
+