diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java
index e1b6cca042..c58b49b552 100644
--- a/study/src/test/java/study/FileTest.java
+++ b/study/src/test/java/study/FileTest.java
@@ -1,53 +1,61 @@
package study;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
-import java.nio.file.Path;
-import java.util.Collections;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
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 URL url = this.getClass().getClassLoader().getResource("nextstep.txt");
final String fileName = "nextstep.txt";
// todo
- final String actual = "";
+ final String actual = url.getPath();
assertThat(actual).endsWith(fileName);
}
/**
* 파일 내용 읽기
- *
- * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
- * File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
+ *
+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
- void 파일의_내용을_읽는다() {
- final String fileName = "nextstep.txt";
-
- // todo
- final Path path = null;
-
- // todo
- final List actual = Collections.emptyList();
+ void 파일의_내용을_읽는다() throws URISyntaxException, IOException {
+ final URL url = this.getClass().getClassLoader().getResource("nextstep.txt");
+ final File file = new File(url.toURI());
+
+ final List actual = new ArrayList<>();
+ try (
+ final FileInputStream fileInputStream = new FileInputStream(file);
+ final BufferedReader bufferedReader = new BufferedReader(
+ new InputStreamReader(fileInputStream, StandardCharsets.UTF_8))
+ ) {
+ actual.add(bufferedReader.readLine());
+ }
assertThat(actual).containsOnly("nextstep");
}
diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java
index 47a79356b6..e29d65f4aa 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -1,5 +1,6 @@
package study;
+import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -24,6 +25,8 @@
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
+ public static final byte[] BYTES = new byte[]{110, 101, 120, 116, 115, 116, 101, 112};
+
/**
* OutputStream 학습하기
*
@@ -46,13 +49,9 @@ class OutputStream_학습_테스트 {
*/
@Test
void OutputStream은_데이터를_바이트로_처리한다() throws IOException {
- final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
- final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
+ final OutputStream outputStream = new ByteArrayOutputStream(BYTES.length);
- /**
- * todo
- * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
- */
+ outputStream.write(BYTES);
final String actual = outputStream.toString();
@@ -73,11 +72,8 @@ class OutputStream_학습_테스트 {
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
final OutputStream outputStream = mock(BufferedOutputStream.class);
- /**
- * todo
- * flush를 사용해서 테스트를 통과시킨다.
- * ByteArrayOutputStream과 어떤 차이가 있을까?
- */
+ outputStream.write(BYTES);
+ outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
@@ -96,6 +92,9 @@ class OutputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try(Writer writer = new OutputStreamWriter(outputStream)) {
+
+ }
verify(outputStream, atLeastOnce()).close();
}
@@ -124,13 +123,10 @@ class InputStream_학습_테스트 {
byte[] bytes = {-16, -97, -92, -87};
final InputStream inputStream = new ByteArrayInputStream(bytes);
- /**
- * todo
- * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
- */
- final String actual = "";
+ final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+ final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
- assertThat(actual).isEqualTo("🤩");
+ assertThat(bufferedReader.readLine()).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
inputStream.close();
}
@@ -148,6 +144,9 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try (Reader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+
+ }
verify(inputStream, atLeastOnce()).close();
}
@@ -169,12 +168,13 @@ class FilterStream_학습_테스트 {
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
- void 필터인_BufferedInputStream를_사용해보자() {
+ void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
- final InputStream bufferedInputStream = null;
+ final BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream, 4);
- final byte[] actual = new byte[0];
+ final byte[] actual = bufferedInputStream.readAllBytes();
+ // 기본 버퍼의 크기는 8192Byte(8KB)
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
@@ -197,7 +197,7 @@ class InputStreamReader_학습_테스트 {
* 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다.
*/
@Test
- void BufferedReader를_사용하여_문자열을_읽어온다() {
+ void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException {
final String emoji = String.join("\r\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
@@ -205,7 +205,14 @@ class InputStreamReader_학습_테스트 {
"");
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
+
final StringBuilder actual = new StringBuilder();
+ try(final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+ while (bufferedReader.ready()) {
+ actual.append(bufferedReader.readLine());
+ actual.append("\r\n");
+ }
+ }
assertThat(actual).hasToString(emoji);
}
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..7754d4f4fe 100644
--- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
@@ -1,19 +1,35 @@
package org.apache.coyote.http11;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.List;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
+import org.apache.coyote.http11.handler.*;
+import org.apache.coyote.http11.request.HttpRequest;
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 final Socket connection;
+ private final List requestHandlers = List.of(
+ new BasicURIHandler(),
+ new IndexPageHandler(),
+ new IndexCSSHandler(),
+ new HttpJavascriptHandler(),
+ new HttpAssetHandler(),
+ new LoginPageHandler(),
+ new LoginHandler(),
+ new RegistrationPageHandler(),
+ new RegistrationHandler()
+ );
+
public Http11Processor(final Socket connection) {
this.connection = connection;
}
@@ -26,20 +42,16 @@ 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());
- outputStream.flush();
+ try (
+ final InputStream inputStream = connection.getInputStream();
+ final OutputStream outputStream = connection.getOutputStream()
+ ) {
+ final HttpRequest httpRequest = HttpRequest.from(inputStream);
+ for (HttpRequestHandler requestHandler : this.requestHandlers) {
+ if (requestHandler.support(httpRequest)) {
+ requestHandler.handle(httpRequest, outputStream);
+ }
+ }
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java
new file mode 100644
index 0000000000..288d1f4389
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11.handler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+public class BasicURIHandler implements HttpRequestHandler {
+ @Override
+ public boolean support(final HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/");
+ }
+
+ @Override
+ public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException {
+ final var responseBody = "Hello world!";
+
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .responseBody(responseBody)
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java
new file mode 100644
index 0000000000..01aa1da04d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.http11.handler;
+
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class HttpAssetHandler implements HttpRequestHandler {
+
+ public static final String ASSETS_PATH_PREFIX = "static/assets/";
+
+ @Override
+ public boolean support(final HttpRequest httpRequest) {
+ return httpRequest.isAssetRequest();
+ }
+
+ @Override
+ public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .contentType("text/javascript")
+ .responseBody(new FileHandler().readFromResourcePath(ASSETS_PATH_PREFIX + httpRequest.getEndPoint()))
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java
new file mode 100644
index 0000000000..41013cb335
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.http11.handler;
+
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class HttpJavascriptHandler implements HttpRequestHandler {
+
+ public static final String JAVASCRIPT_PATH_PREFIX = "static/js/";
+
+ @Override
+ public boolean support(final HttpRequest httpRequest) {
+ return httpRequest.isJavascriptRequest();
+ }
+
+ @Override
+ public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .contentType("text/javascript")
+ .responseBody(new FileHandler().readFromResourcePath(JAVASCRIPT_PATH_PREFIX + httpRequest.getEndPoint()))
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java
new file mode 100644
index 0000000000..24bf10e4e0
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java
@@ -0,0 +1,11 @@
+package org.apache.coyote.http11.handler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.coyote.http11.request.HttpRequest;
+
+public interface HttpRequestHandler {
+ boolean support(HttpRequest httpRequest);
+
+ void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException;
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java
new file mode 100644
index 0000000000..4c323ba7c8
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.http11.handler;
+
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class IndexCSSHandler implements HttpRequestHandler {
+
+ public static final String CSS_PATH_PREFIX = "static/css/";
+
+ @Override
+ public boolean support(final HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/css/styles.css");
+ }
+
+ @Override
+ public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .contentType("text/css")
+ .responseBody(new FileHandler().readFromResourcePath(CSS_PATH_PREFIX + httpRequest.getEndPoint()))
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java
new file mode 100644
index 0000000000..2828c0b3b6
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java
@@ -0,0 +1,24 @@
+package org.apache.coyote.http11.handler;
+
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class IndexPageHandler implements HttpRequestHandler {
+ @Override
+ public boolean support(final HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/index.html");
+ }
+
+ @Override
+ public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .responseBody(new FileHandler().readFromResourcePath("static/index.html"))
+ .build(outputStream);
+
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java
new file mode 100644
index 0000000000..423fcba9e8
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java
@@ -0,0 +1,81 @@
+package org.apache.coyote.http11.handler;
+
+import java.util.Map;
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.coyote.http11.*;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.response.HttpResponse;
+import org.apache.coyote.http11.session.Session;
+import org.apache.coyote.http11.session.SessionManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.UUID;
+
+public class LoginHandler implements HttpRequestHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
+
+ @Override
+ public boolean support(HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("POST") && httpRequest.isUriEqualTo("/login");
+ }
+
+ @Override
+ public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException {
+ final Map requestBody = httpRequest.getRequestBodyAsMap();
+ final Optional account = Optional.ofNullable(requestBody.get("account"));
+ final Optional password = Optional.ofNullable(requestBody.get("password"));
+
+ if (account.isEmpty() || password.isEmpty()) {
+ returnUnauthorizedPage(outputStream);
+ return;
+ }
+ verifyAccount(account.get(), password.get(), outputStream);
+ }
+
+ private void verifyAccount(String account, String password, OutputStream outputStream) throws IOException {
+ final Optional user = InMemoryUserRepository.findByAccount(account);
+ if (user.isEmpty() || !user.get().checkPassword(password)) {
+ returnUnauthorizedPage(outputStream);
+ return;
+ }
+ logAccount(user.get());
+ redirectIndexPage(outputStream, user.get());
+ }
+
+ private void returnUnauthorizedPage(OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .responseBody(new FileHandler().readFromResourcePath("static/401.html"))
+ .responseStatus("401")
+ .build(outputStream);
+ httpResponse.flush();
+ }
+
+ private void redirectIndexPage(OutputStream outputStream, User user) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .redirect("/index.html")
+ .build(outputStream);
+
+ final String sessionId = UUID.randomUUID().toString();
+ httpResponse.addCookie("JSESSIONID", sessionId);
+ saveUserToSession(user, sessionId);
+
+ httpResponse.flush();
+ }
+
+ private void saveUserToSession(User user, String sessionId) {
+ final Session session = new Session(sessionId);
+ session.setAttribute(sessionId, user);
+ SessionManager.add(session);
+ }
+
+ private void logAccount(User user) {
+ log.info("user : {}", user);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java
new file mode 100644
index 0000000000..bae473215b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java
@@ -0,0 +1,35 @@
+package org.apache.coyote.http11.handler;
+
+import nextstep.jwp.model.User;
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+import org.apache.coyote.http11.session.SessionManager;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Optional;
+
+public class LoginPageHandler implements HttpRequestHandler {
+
+ @Override
+ public boolean support(HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/login") && !httpRequest.hasQueryParameter();
+ }
+
+ @Override
+ public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException {
+ final Optional sessionId = httpRequest.getSessionId();
+ if (sessionId.isPresent() && SessionManager.hasSessionWithAttributeType(sessionId.get(), User.class)) {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .redirect("/index.html")
+ .build(outputStream);
+ httpResponse.flush();
+ }
+
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .responseBody(new FileHandler().readFromResourcePath("static/login.html"))
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java
new file mode 100644
index 0000000000..3cee4493c2
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java
@@ -0,0 +1,30 @@
+package org.apache.coyote.http11.handler;
+
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+
+public class RegistrationHandler implements HttpRequestHandler {
+ @Override
+ public boolean support(HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("POST") && httpRequest.isUriEqualTo("/register");
+ }
+
+ @Override
+ public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException {
+ final Map requestBody = httpRequest.getRequestBodyAsMap();
+ final User user = new User(requestBody.get("account"), requestBody.get("password"), requestBody.get("email"));
+ InMemoryUserRepository.save(user);
+
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .redirect("/index.html")
+ .build(outputStream);
+ httpResponse.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java
new file mode 100644
index 0000000000..962087281f
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java
@@ -0,0 +1,24 @@
+package org.apache.coyote.http11.handler;
+
+import org.apache.coyote.http11.resource.FileHandler;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.response.HttpResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class RegistrationPageHandler implements HttpRequestHandler {
+ @Override
+ public boolean support(HttpRequest httpRequest) {
+ return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/register");
+ }
+
+ @Override
+ public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException {
+ final HttpResponse httpResponse = new HttpResponse.Builder()
+ .responseBody(new FileHandler().readFromResourcePath("static/register.html"))
+ .build(outputStream);
+
+ httpResponse.flush();
+ }
+}
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..54b9e32c79
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java
@@ -0,0 +1,99 @@
+package org.apache.coyote.http11.request;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.StringTokenizer;
+import org.apache.coyote.http11.resource.Cookies;
+
+public class HttpRequest {
+
+ private String uri;
+
+ private String method;
+
+ private String requestBody;
+
+ private Cookies cookies;
+
+ public static HttpRequest from(InputStream inputStream) {
+ return HttpRequestParser.parseFromSocket(inputStream);
+ }
+
+ public HttpRequest(String uri, String method, String requestBody, Cookies cookies) {
+ this.uri = uri;
+ this.method = method;
+ this.requestBody = requestBody;
+ this.cookies = cookies;
+ }
+
+ public boolean isMethodEqualTo(final String method) {
+ return Objects.equals(this.method, method);
+ }
+
+ public boolean isUriEqualTo(final String uri) {
+ final int startPointOfQueryParameter = this.uri.lastIndexOf("?");
+ if (startPointOfQueryParameter != -1) {
+ final String uriWithoutQueryParameter = this.uri.substring(0, startPointOfQueryParameter);
+ return Objects.equals(uriWithoutQueryParameter, uri);
+ }
+ return Objects.equals(this.uri, uri);
+ }
+
+ public boolean isJavascriptRequest() {
+ return isMethodEqualTo("GET") && this.uri.startsWith("/js/");
+ }
+
+ public boolean isAssetRequest() {
+ return isMethodEqualTo("GET") && this.uri.startsWith("/assets/");
+ }
+
+ public String getEndPoint() {
+ return this.uri.substring(this.uri.lastIndexOf("/") + 1, this.uri.length());
+ }
+
+ public Map getQueryParameters() {
+ if (!hasQueryParameter()) {
+ return Map.of();
+ }
+
+ final String parameters = this.uri.substring(this.uri.lastIndexOf("?") + 1, this.uri.length());
+ return parseParametersIntoMap(parameters);
+ }
+
+ public boolean hasQueryParameter() {
+ return !Objects.equals(this.uri.lastIndexOf("?"), -1);
+ }
+
+ public Optional getQueryParameter(String parameter) {
+ return Optional.ofNullable(this.getQueryParameters().get(parameter));
+ }
+
+ public Map getRequestBodyAsMap() {
+ return parseParametersIntoMap(this.requestBody);
+ }
+
+ private HashMap parseParametersIntoMap(String parameters) {
+ final HashMap queryParameters = new HashMap<>();
+ final StringTokenizer stringTokenizer = new StringTokenizer(parameters, "&");
+ while (stringTokenizer.hasMoreTokens()) {
+ final String parameter = stringTokenizer.nextToken();
+ parseParameterIntoMap(queryParameters, parameter);
+ }
+ return queryParameters;
+ }
+
+ private static void parseParameterIntoMap(final HashMap queryParameters, final String parameter) {
+ final StringTokenizer stringTokenizer = new StringTokenizer(parameter, "=");
+ if (!stringTokenizer.hasMoreTokens()) {
+ return;
+ }
+ queryParameters.put(stringTokenizer.nextToken(), stringTokenizer.nextToken());
+ }
+
+ public Optional getSessionId() {
+ return cookies.get("JSESSIONID");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java
new file mode 100644
index 0000000000..6a27de3a86
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java
@@ -0,0 +1,87 @@
+package org.apache.coyote.http11.request;
+
+import java.io.InputStream;
+import java.util.Objects;
+import nextstep.jwp.exception.UncheckedServletException;
+import org.apache.coyote.http11.resource.Cookies;
+import org.apache.coyote.http11.Http11Processor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class HttpRequestParser {
+
+ private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
+
+ public static HttpRequest parseFromSocket(InputStream inputStream) {
+ try {
+ final var bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+
+ final StringTokenizer stringTokenizer = new StringTokenizer(bufferedReader.readLine());
+ final String httpRequestMethod = stringTokenizer.nextToken();
+ final String httpRequestUri = stringTokenizer.nextToken();
+
+ final Map headers = getHeaders(bufferedReader);
+ final Cookies cookies = getCookies(headers);
+ final String requestBody = getRequestBody(bufferedReader);
+
+ return new HttpRequest(httpRequestUri, httpRequestMethod, requestBody, cookies);
+ } catch (IOException | UncheckedServletException e) {
+ log.error(e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Cookies getCookies(Map headers) throws IOException {
+ final Cookies cookies = new Cookies();
+ final String cookiesHeader = headers.get("Cookie");
+
+ if (Objects.isNull(cookiesHeader)) {
+ return cookies;
+ }
+
+ final StringTokenizer stringTokenizer = new StringTokenizer(cookiesHeader, "; ");
+ while (stringTokenizer.hasMoreTokens()) {
+ final String cookie = stringTokenizer.nextToken();
+ addCookie(cookies, cookie);
+ }
+
+ return cookies;
+ }
+
+ private static void addCookie(final Cookies cookies, final String cookieEntry) {
+ final StringTokenizer stringTokenizer = new StringTokenizer(cookieEntry, "=");
+ cookies.add(
+ stringTokenizer.nextToken(),
+ stringTokenizer.nextToken()
+ );
+ }
+
+ private static Map getHeaders(BufferedReader bufferedReader) throws IOException {
+ final Map requestHeaders = new HashMap<>();
+ String header;
+ while ((header = bufferedReader.readLine()).length() != 0) {
+ final StringTokenizer stringTokenizer = new StringTokenizer(header, ": ");
+ requestHeaders.put(
+ stringTokenizer.nextToken(),
+ stringTokenizer.nextToken()
+ );
+ }
+ return requestHeaders;
+ }
+
+ private static String getRequestBody(BufferedReader bufferedReader) throws IOException {
+ final StringBuilder requestBody = new StringBuilder();
+ while (bufferedReader.ready()) {
+ requestBody.append((char) bufferedReader.read());
+ }
+ return requestBody.toString();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java
new file mode 100644
index 0000000000..1a55528e17
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.http11.resource;
+
+import java.util.Objects;
+
+public class Cookie {
+
+ private String key;
+
+ private String value;
+
+ public Cookie(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public boolean isKeyName(String key) {
+ return Objects.equals(this.key, key);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java
new file mode 100644
index 0000000000..6e938b8cdc
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java
@@ -0,0 +1,26 @@
+package org.apache.coyote.http11.resource;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+public class Cookies implements Iterable {
+ private List cookies = new ArrayList<>();
+
+ @Override
+ public Iterator iterator() {
+ return cookies.iterator();
+ }
+
+ public void add(String key, String value) {
+ cookies.add(new Cookie(key, value));
+ }
+
+ public Optional get(String key) {
+ final Optional cookie = cookies.stream()
+ .filter(coo -> coo.isKeyName(key))
+ .findAny();
+ return cookie.map(Cookie::getValue);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java
new file mode 100644
index 0000000000..151cbb95c7
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java
@@ -0,0 +1,33 @@
+package org.apache.coyote.http11.resource;
+
+import java.io.*;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class FileHandler {
+ public String readFromResourcePath(String path) throws IOException {
+ final var responseBody = new StringBuilder();
+
+ final URL indexPageURL = this.getClass().getClassLoader().getResource(path);
+ final File indexFile;
+ try {
+ indexFile = new File(indexPageURL.toURI());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ try (
+ final FileInputStream fileInputStream = new FileInputStream(indexFile);
+ final BufferedReader bufferedReader = new BufferedReader(
+ new InputStreamReader(fileInputStream, StandardCharsets.UTF_8))
+ ) {
+ while (bufferedReader.ready()) {
+ responseBody
+ .append(bufferedReader.readLine())
+ .append(System.lineSeparator());
+ }
+ }
+
+ return responseBody.toString();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java
new file mode 100644
index 0000000000..69c3c45336
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11.response;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class HttpRedirectResponse extends HttpResponse {
+
+ protected final String redirectUri;
+
+ public HttpRedirectResponse(
+ final OutputStream outputStream,
+ final String redirectUri
+ ) {
+ super(outputStream, "302", null, null, null);
+ this.redirectUri = redirectUri;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ outputStream.write(HttpRedirectResponseParser.parseToBytes(this));
+ outputStream.flush();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java
new file mode 100644
index 0000000000..426dec99dd
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java
@@ -0,0 +1,13 @@
+package org.apache.coyote.http11.response;
+
+public class HttpRedirectResponseParser extends HttpResponseParser {
+ public static byte[] parseToBytes(final HttpRedirectResponse httpResponse) {
+ final StringBuilder response = new StringBuilder();
+
+ responseStatus(httpResponse, response);
+ cookies(httpResponse, response);
+ header("Location", httpResponse.redirectUri, response);
+
+ return response.toString().getBytes();
+ }
+}
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..94b0f6fded
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java
@@ -0,0 +1,96 @@
+package org.apache.coyote.http11.response;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.coyote.http11.resource.Cookies;
+
+public class HttpResponse {
+
+ protected OutputStream outputStream;
+
+ protected String responseStatus;
+
+ protected String contentType;
+
+ protected String charSet;
+
+ protected int contentLength;
+
+ protected Cookies cookies = new Cookies();
+
+ protected String responseBody;
+
+ protected HttpResponse(OutputStream outputStream, String responseStatus, String contentType, String charSet, String responseBody) {
+ this.outputStream = outputStream;
+ this.responseStatus = responseStatus;
+ this.contentType = contentType;
+ this.charSet = charSet;
+ this.responseBody = responseBody;
+ this.contentLength = this.responseBody != null ? this.responseBody.getBytes().length : 0;
+ }
+
+ public void flush() throws IOException {
+ outputStream.write(HttpResponseParser.parseToBytes(this));
+ outputStream.flush();
+ }
+
+ public void addCookie(String key, String value) {
+ this.cookies.add(key, value);
+ }
+
+ public static class Builder {
+ private String responseStatus;
+
+ private String contentType;
+
+ private String charSet;
+
+ private String responseBody;
+
+ public Builder responseStatus(String responseStatus) {
+ this.responseStatus = responseStatus;
+ return this;
+ }
+
+ public Builder contentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ public Builder charSet(String charSet) {
+ this.charSet = charSet;
+ return this;
+ }
+
+ public Builder responseBody(String responseBody) {
+ this.responseBody = responseBody;
+ return this;
+ }
+
+ public RedirectBuilder redirect(final String redirectUrl) {
+ return new RedirectBuilder(redirectUrl);
+ }
+
+ public HttpResponse build(OutputStream outputStream) {
+ return new HttpResponse(
+ outputStream,
+ this.responseStatus == null ? "200 OK" : this.responseStatus,
+ this.contentType == null ? "text/html" : this.contentType,
+ this.charSet == null ? "utf-8" : this.charSet,
+ this.responseBody == null ? "" : this.responseBody
+ );
+ }
+ }
+
+ public static class RedirectBuilder {
+ protected RedirectBuilder(final String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ private final String redirectUri;
+
+ public HttpResponse build(OutputStream outputStream) {
+ return new HttpRedirectResponse(outputStream, redirectUri);
+ }
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java
new file mode 100644
index 0000000000..0911de0fb1
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java
@@ -0,0 +1,82 @@
+package org.apache.coyote.http11.response;
+
+import java.util.Objects;
+import org.apache.coyote.http11.resource.Cookie;
+
+public class HttpResponseParser {
+
+ protected HttpResponseParser() {
+ }
+
+ public static byte[] parseToBytes(final HttpResponse httpResponse) {
+ final StringBuilder response = new StringBuilder();
+
+ responseStatus(httpResponse, response);
+ contentType(httpResponse, response);
+ charset(httpResponse, response);
+
+ cookies(httpResponse, response);
+ contentLength(httpResponse, response);
+ responseBody(httpResponse, response);
+
+ return response.toString().getBytes();
+ }
+
+ protected static void responseBody(final HttpResponse httpResponse, final StringBuilder response) {
+ if (Objects.nonNull(httpResponse.responseBody)) {
+ response.append(httpResponse.responseBody);
+ }
+ }
+
+ protected static void contentLength(final HttpResponse httpResponse, final StringBuilder response) {
+ response.append("Content-Length: ")
+ .append(httpResponse.contentLength)
+ .append(" ")
+ .append(System.lineSeparator());
+ response.append(System.lineSeparator());
+ }
+
+ protected static void cookies(final HttpResponse httpResponse, final StringBuilder response) {
+ for (Cookie cookie : httpResponse.cookies) {
+ response.append("Set-Cookie: ")
+ .append(cookie.getKey())
+ .append("=")
+ .append(cookie.getValue())
+ .append(" ")
+ .append(System.lineSeparator());
+ }
+ }
+
+ protected static void responseStatus(final HttpResponse httpResponse, final StringBuilder response) {
+ response.append("HTTP/1.1 ")
+ .append(httpResponse.responseStatus)
+ .append(" ")
+ .append(System.lineSeparator());
+ }
+
+ protected static void charset(final HttpResponse httpResponse, final StringBuilder response) {
+ if (Objects.nonNull(httpResponse.charSet)) {
+ response
+ .append(";charset=")
+ .append(httpResponse.charSet)
+ .append(" ");
+ }
+ response.append(System.lineSeparator());
+ }
+
+ protected static void header(final String headerName, final String headerValue, final StringBuilder response) {
+ if (Objects.nonNull(headerName) && Objects.nonNull(headerValue)) {
+ response.append(headerName).append(": ")
+ .append(headerValue)
+ .append(" ");
+ }
+ response.append(System.lineSeparator());
+ }
+
+ protected static void contentType(final HttpResponse httpResponse, final StringBuilder response) {
+ if (Objects.nonNull(httpResponse.contentType)) {
+ response.append("Content-Type: ")
+ .append(httpResponse.contentType);
+ }
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java
new file mode 100644
index 0000000000..703323de93
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java
@@ -0,0 +1,36 @@
+package org.apache.coyote.http11.session;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Session {
+
+ private final String id;
+
+ private final Map attributes = new HashMap<>();
+
+ public Session(final String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public Object getAttribute(final String name) {
+ return attributes.get(name);
+ }
+
+ public void setAttribute(final String name, final Object value) {
+ attributes.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ attributes.remove(name);
+ }
+
+ public boolean hasAttributeType(Class> attributeType) {
+ return attributes.values().stream()
+ .anyMatch(attribute -> attribute.getClass().equals(attributeType));
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java
new file mode 100644
index 0000000000..8c4ab30480
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java
@@ -0,0 +1,30 @@
+package org.apache.coyote.http11.session;
+
+import nextstep.jwp.model.User;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class SessionManager {
+
+ private static Map sessions = new HashMap<>();
+
+ public static void add(Session session) {
+ sessions.put(session.getId(), session);
+ }
+
+ public static Optional findSession(String id) {
+ return Optional.ofNullable(sessions.get(id));
+ }
+
+ public static void remove(Session session) {
+ sessions.remove(session.getId());
+ }
+
+ public static boolean hasSessionWithAttributeType(String sessionId, Class> attributeType) {
+ final Optional session = findSession(sessionId);
+ return session.map(value -> value.hasAttributeType(attributeType))
+ .orElse(false);
+ }
+}
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 @@