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..41678bd585 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -1,8 +1,6 @@
package study;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.*;
import java.io.*;
@@ -12,21 +10,23 @@
/**
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
- *
+ *
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
+ *
* Stream은 데이터를 바이트로 읽고 쓴다.
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
*/
+@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
/**
* OutputStream 학습하기
- *
+ *
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
* public abstract void write(int b) throws IOException;
@@ -39,7 +39,7 @@ class OutputStream_학습_테스트 {
* OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
* 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
* 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
- *
+ *
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* write(byte[] data)
와 write(byte b[], int off, int len)
메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
@@ -49,10 +49,7 @@ class OutputStream_학습_테스트 {
final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
final OutputStream outputStream = new ByteArrayOutputStream(bytes.length);
- /**
- * todo
- * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
- */
+ outputStream.write(bytes);
final String actual = outputStream.toString();
@@ -63,7 +60,7 @@ class OutputStream_학습_테스트 {
/**
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
+ *
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
@@ -73,11 +70,7 @@ class OutputStream_학습_테스트 {
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
final OutputStream outputStream = mock(BufferedOutputStream.class);
- /**
- * todo
- * flush를 사용해서 테스트를 통과시킨다.
- * ByteArrayOutputStream과 어떤 차이가 있을까?
- */
+ outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
@@ -91,11 +84,9 @@ class OutputStream_학습_테스트 {
void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
final OutputStream outputStream = mock(OutputStream.class);
- /**
- * todo
- * try-with-resources를 사용한다.
- * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
- */
+ try (outputStream) {
+
+ }
verify(outputStream, atLeastOnce()).close();
}
@@ -103,12 +94,12 @@ class OutputStream_학습_테스트 {
/**
* InputStream 학습하기
- *
+ *
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
* InputStream의 read() 메서드는 기반 메서드이다.
* public abstract int read() throws IOException;
- *
+ *
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
@@ -124,11 +115,7 @@ class InputStream_학습_테스트 {
byte[] bytes = {-16, -97, -92, -87};
final InputStream inputStream = new ByteArrayInputStream(bytes);
- /**
- * todo
- * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
- */
- final String actual = "";
+ final String actual = new String(inputStream.readAllBytes());
assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
@@ -143,11 +130,9 @@ class InputStream_학습_테스트 {
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
final InputStream inputStream = mock(InputStream.class);
- /**
- * todo
- * try-with-resources를 사용한다.
- * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
- */
+ try (inputStream) {
+
+ }
verify(inputStream, atLeastOnce()).close();
}
@@ -155,7 +140,7 @@ class InputStream_학습_테스트 {
/**
* FilterStream 학습하기
- *
+ *
* 필터는 필터 스트림, reader, writer로 나뉜다.
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
@@ -169,12 +154,12 @@ class FilterStream_학습_테스트 {
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
- void 필터인_BufferedInputStream를_사용해보자() {
+ void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
- final InputStream bufferedInputStream = null;
+ final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
- final byte[] actual = new byte[0];
+ final byte[] actual = bufferedInputStream.readAllBytes();
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
@@ -197,17 +182,22 @@ class InputStreamReader_학습_테스트 {
* 필터인 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 StringBuilder actual = new StringBuilder();
-
- assertThat(actual).hasToString(emoji);
+ try (final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));){
+ StringBuilder actual = new StringBuilder();
+ String line = bufferedReader.readLine();
+ while (line != null) {
+ actual.append(line).append("\r\n");
+ line = bufferedReader.readLine();
+ }
+ assertThat(actual).hasToString(emoji);
+ }
}
}
}
diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java
new file mode 100644
index 0000000000..8b68d9a417
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/catalina/Session.java
@@ -0,0 +1,34 @@
+package org.apache.catalina;
+
+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 String getId() {
+ return this.id;
+ }
+
+ public Object getAttribute(final String name) {
+ return values.get(name);
+ }
+
+ public void setAttribute(final String name, final Object value) {
+ this.values.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ this.values.remove(name);
+ }
+
+ public void invalidate() {
+ this.values.clear();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java
new file mode 100644
index 0000000000..3e550f2faf
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java
@@ -0,0 +1,21 @@
+package org.apache.catalina;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SessionManager {
+
+ private static final Map SESSIONS = new HashMap<>();
+
+ public void add(final Session session) {
+ SESSIONS.put(session.getId(), session);
+ }
+
+ public Session findSession(final String id) {
+ return SESSIONS.get(id);
+ }
+
+ public void remove(final String id) {
+ SESSIONS.remove(id);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java
new file mode 100644
index 0000000000..7f64dbebf2
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java
@@ -0,0 +1,38 @@
+package org.apache.coyote.http11;
+
+import java.util.Arrays;
+
+public enum ContentType {
+ TEXT_HTML("text/html", "html"),
+ CSS("text/css", "css"),
+ APPLICATION_JSON("application/json", "json"),
+ JAVASCRIPT("application/javascript", "js"),
+ ;
+ private static final String DEFAULT_UTF8 = "charset=utf-8";
+ private static final String EXT_SEPARATOR = ".";
+
+ private final String type;
+ private final String ext;
+
+ ContentType(String type, String ext) {
+ this.type = type;
+ this.ext = ext;
+ }
+
+ public static ContentType from(String fileName) {
+
+ int index = fileName.indexOf(EXT_SEPARATOR);
+ if (index == -1) {
+ throw new IllegalArgumentException("지원하지 않는 ContentType입니다. fileName: " + fileName);
+ }
+ String ext = fileName.substring(index + 1);
+ return Arrays.stream(values())
+ .filter(type -> type.ext.equalsIgnoreCase(ext))
+ .findAny()
+ .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 ContentType입니다. fileName: " + ext));
+ }
+
+ public String getType() {
+ return String.format("%s;%s", type, DEFAULT_UTF8);
+ }
+}
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..a7b26fa902 100644
--- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
@@ -1,16 +1,38 @@
package org.apache.coyote.http11;
+import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.exception.UncheckedServletException;
+import nextstep.jwp.model.User;
+import org.apache.catalina.Session;
+import org.apache.catalina.SessionManager;
import org.apache.coyote.Processor;
+import org.apache.coyote.requests.HttpCookie;
+import org.apache.coyote.requests.RequestBody;
+import org.apache.coyote.requests.RequestHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.net.Socket;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.StringJoiner;
+
+import static org.apache.coyote.http11.ContentType.TEXT_HTML;
public class Http11Processor implements Runnable, Processor {
private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
+ private static final SessionManager sessionManager = new SessionManager();
+ private static final String HTTP_11 = "HTTP/1.1";
+ private static final String INDEX_URI = "/index";
+ private static final String REGISTER_URI = "/register";
+ private static final String LOGIN_URI = "/login";
+ private static final String UNAUTHORIZED_URI = "/401.html";
private final Socket connection;
@@ -27,21 +49,198 @@ public void run() {
@Override
public void process(final Socket connection) {
try (final var inputStream = connection.getInputStream();
- final var outputStream = connection.getOutputStream()) {
+ final var outputStream = connection.getOutputStream();
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream))) {
+ String line = bufferedReader.readLine();
+ if (line == null) {
+ return;
+ }
+
+ String httpMethod = Arrays.stream(line.split(" "))
+ .findFirst()
+ .orElseGet(() -> "");
- final var responseBody = "Hello world!";
+ String uri = Arrays.stream(line.split(" "))
+ .filter(it -> it.startsWith("/"))
+ .findAny()
+ .orElseGet(() -> "");
- 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);
+ RequestHeader requestHeader = readHeader(bufferedReader);
+ RequestBody requestBody = readBody(bufferedReader, requestHeader);
- outputStream.write(response.getBytes());
- outputStream.flush();
+ HttpResponse httpResponse = routeByHttpMethod(uri, httpMethod, requestHeader, requestBody);
+ String header = createHeader(httpResponse);
+
+ bufferedWriter.write(header + httpResponse.getBody());
+ bufferedWriter.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
}
+
+ private HttpResponse routeByHttpMethod(String uri, String httpMethod, RequestHeader requestHeader, RequestBody requestBody) throws IOException {
+ if (httpMethod.equals("POST")) {
+ return routePost(uri, requestHeader, requestBody);
+ }
+
+ return createHttpResponse(uri, requestHeader);
+ }
+
+ private HttpResponse routePost(String uri, RequestHeader requestHeader, RequestBody requestBody) throws IOException {
+ if (REGISTER_URI.equals(uri)) {
+ return doRegister(requestBody);
+ }
+
+ if (LOGIN_URI.equals(uri)) {
+ return doLogin(requestHeader, requestBody);
+ }
+ return createHttpResponse(uri, requestHeader);
+ }
+
+ private RequestHeader readHeader(BufferedReader bufferedReader) throws IOException {
+ final StringBuilder stringBuilder = new StringBuilder();
+ for (String line = bufferedReader.readLine();
+ !"".equals(line); line = bufferedReader.readLine()) {
+ stringBuilder.append(line).append("\r\n");
+ }
+ return RequestHeader.from(stringBuilder.toString());
+ }
+
+ private RequestBody readBody(BufferedReader bufferedReader, RequestHeader requestHeader) throws IOException {
+ final String contentLength = requestHeader.get("Content-Length");
+ if (contentLength == null) {
+ return new RequestBody();
+ }
+ final int length = Integer.parseInt(contentLength);
+ char[] buffer = new char[length];
+ bufferedReader.read(buffer, 0, length);
+ return new RequestBody(new String(buffer));
+ }
+
+ private HttpResponse createHttpResponse(String uri, RequestHeader requestHeader) throws IOException {
+ String path = uri;
+ int index = uri.indexOf("?");
+ if (index != -1) {
+ path = uri.substring(0, index);
+ }
+
+ HttpCookie cookie = HttpCookie.from(requestHeader.get("Cookie"));
+ if (LOGIN_URI.equals(path)
+ && cookie != null
+ && sessionManager.findSession(cookie.getJSessionId(false)) != null) {
+ return ViewResolver.resolveView(INDEX_URI);
+ }
+ return ViewResolver.routePath(path);
+ }
+
+ private String createHeader(HttpResponse response) {
+ StringJoiner stringJoiner = new StringJoiner("\r\n");
+ HttpStatus httpStatus = response.getHttpStatus();
+
+ stringJoiner.add(String.format("%s %d %s ", HTTP_11, httpStatus.getCode(), httpStatus.getMessage()));
+
+ for (Map.Entry entry : response.getHeaders().entrySet()) {
+ stringJoiner.add(String.format("%s: %s ", entry.getKey(), entry.getValue()));
+ }
+
+ if (httpStatus.equals(HttpStatus.FOUND)) {
+ stringJoiner.add(toHeaderFormat("Location", response.getBody()));
+ stringJoiner.add("\r\n");
+ return stringJoiner.toString();
+ }
+ stringJoiner.add(toHeaderFormat("Content-Type", response.getContentType()));
+ stringJoiner.add(String.format("%s %s ", "Content-Length:", response.getBody().getBytes().length));
+ stringJoiner.add("\r\n");
+ return stringJoiner.toString();
+ }
+
+ private String toHeaderFormat(String name, String value) {
+ return String.format("%s: %s ", name, value);
+ }
+
+ private HttpResponse doLogin(RequestHeader requestHeader, RequestBody requestBody) throws IOException {
+ String query = requestBody.getItem();
+ if (query.isBlank()) {
+ return new HttpResponse(LOGIN_URI, HttpStatus.FOUND, TEXT_HTML);
+ }
+
+ String account = "";
+ String password = "";
+
+ for (String parameter : query.split("&")) {
+ int idx = parameter.indexOf("=");
+ String key = parameter.substring(0, idx);
+ String value = parameter.substring(idx + 1);
+ if ("account".equals(key)) {
+ account = value;
+ }
+ if ("password".equals(key)) {
+ password = value;
+ }
+ }
+
+ User user = InMemoryUserRepository.findByAccount(account)
+ .orElse(null);
+
+ if (user != null && user.checkPassword(password)) {
+ HttpCookie cookie = HttpCookie.from(requestHeader.get("Cookie"));
+ HttpResponse httpResponse = new HttpResponse(INDEX_URI, HttpStatus.FOUND, TEXT_HTML);
+
+ if (cookie.getJSessionId(false) == null) {
+ String jSessionId = cookie.getJSessionId(true);
+ httpResponse.addHeader("Set-Cookie", "JSESSIONID=" + jSessionId);
+ Session session = new Session(jSessionId);
+ session.setAttribute("user", user);
+ sessionManager.add(session);
+ }
+
+ if (log.isInfoEnabled()) {
+ log.info(String.format("%s %s", "로그인 성공!", user));
+ }
+ return httpResponse;
+ }
+
+ return ViewResolver.resolveView(UNAUTHORIZED_URI);
+ }
+
+ private HttpResponse doRegister(RequestBody requestBody) throws IOException {
+ String query = requestBody.getItem();
+ if (query.isBlank()) {
+ return ViewResolver.resolveView(REGISTER_URI);
+ }
+
+ String account = "";
+ String password = "";
+ String email = "";
+
+ for (String parameter : query.split("&")) {
+ int idx = parameter.indexOf("=");
+ String key = parameter.substring(0, idx);
+ String value = parameter.substring(idx + 1);
+
+ if ("account".equals(key)) {
+ account = value;
+ }
+ if ("password".equals(key)) {
+ password = value;
+ }
+ if ("email".equals(key)) {
+ email = value;
+ }
+ }
+
+ if (account.isBlank() || password.isBlank() || email.isBlank()) {
+ return ViewResolver.resolveView(REGISTER_URI);
+ }
+
+ User registUser = new User(account, password, email);
+ InMemoryUserRepository.save(registUser);
+
+ if (log.isInfoEnabled()) {
+ log.info(String.format("%s %s", "회원가입 성공!", registUser));
+ }
+
+ return ViewResolver.resolveView(LOGIN_URI);
+ }
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java
new file mode 100644
index 0000000000..a332c7dd9b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java
@@ -0,0 +1,38 @@
+package org.apache.coyote.http11;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpResponse {
+
+ private final String body;
+ private final HttpStatus httpStatus;
+ private final ContentType contentType;
+ private final Map headers = new HashMap<>();
+
+ public HttpResponse(String body, HttpStatus httpStatus, ContentType contentType) {
+ this.body = body;
+ this.httpStatus = httpStatus;
+ this.contentType = contentType;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public HttpStatus getHttpStatus() {
+ return httpStatus;
+ }
+
+ public String getContentType() {
+ return contentType.getType();
+ }
+
+ public void addHeader(String key, String value) {
+ headers.put(key, value);
+ }
+
+ public Map getHeaders() {
+ return Map.copyOf(headers);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java
new file mode 100644
index 0000000000..0c214d1da6
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java
@@ -0,0 +1,26 @@
+package org.apache.coyote.http11;
+
+public enum HttpStatus {
+ OK(200, "OK"),
+ CREATED(201, "Created"),
+ FOUND(302, "Found"),
+ UNAUTHORIZED(401, "Unauthorized"),
+ NOT_FOUND(404, "Not Found"),
+ ;
+
+ private final int code;
+ private final String message;
+
+ HttpStatus(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java
new file mode 100644
index 0000000000..d433249461
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java
@@ -0,0 +1,45 @@
+package org.apache.coyote.http11;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+
+import static org.apache.coyote.http11.ContentType.TEXT_HTML;
+
+public class ViewResolver {
+ private static final String DEFAULT_FILE_ROUTE = "static";
+
+ private ViewResolver() {
+ }
+
+ public static HttpResponse resolveView(String path) throws IOException {
+ String filePath = path;
+ if (!filePath.contains(".")) {
+ filePath += ".html";
+ }
+
+ final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
+ final URL resource = systemClassLoader.getResource(String.format("%s%s", DEFAULT_FILE_ROUTE, filePath));
+
+ if (resource == null) {
+ final URL notFoundUrl = systemClassLoader.getResource(String.format("%s/%s", DEFAULT_FILE_ROUTE, "404.html"));
+ File notFound = new File(notFoundUrl.getPath());
+ String body = new String(Files.readAllBytes(notFound.toPath()));
+ return new HttpResponse(body, HttpStatus.NOT_FOUND, ContentType.from(notFound.getName()));
+ }
+
+ File file = new File(resource.getPath());
+ String body = new String(Files.readAllBytes(file.toPath()));
+
+ return new HttpResponse(body, HttpStatus.OK, ContentType.from(file.getName()));
+ }
+
+ public static HttpResponse routePath(String path) throws IOException {
+ if (path.equals("/")) {
+ return new HttpResponse("Hello world!", HttpStatus.OK, TEXT_HTML);
+ }
+
+ return ViewResolver.resolveView(path);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java
new file mode 100644
index 0000000000..7b4045681f
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java
@@ -0,0 +1,40 @@
+package org.apache.coyote.requests;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toMap;
+
+public class HttpCookie {
+ private final Map items;
+
+ public HttpCookie(Map items) {
+ this.items = items;
+ }
+
+ public static HttpCookie from(String string) {
+ if (string == null || string.isBlank()) {
+ return new HttpCookie(new HashMap<>());
+ }
+ return Arrays.stream(string.split(" "))
+ .map(line -> line.split("="))
+ .collect(collectingAndThen(
+ toMap(line -> line[0], line -> line[1]),
+ HttpCookie::new
+ ));
+ }
+
+ public String get(String key) {
+ return items.get(key);
+ }
+
+ public String getJSessionId(boolean create) {
+ if (create) {
+ items.put("JSESSIONID", UUID.randomUUID().toString());
+ }
+ return get("JSESSIONID");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java
new file mode 100644
index 0000000000..261368be6c
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java
@@ -0,0 +1,18 @@
+package org.apache.coyote.requests;
+
+public class RequestBody {
+
+ private final String item;
+
+ public RequestBody() {
+ this.item = "";
+ }
+
+ public RequestBody(String item) {
+ this.item = item;
+ }
+
+ public String getItem() {
+ return item;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java
new file mode 100644
index 0000000000..5cc3cc03c7
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java
@@ -0,0 +1,28 @@
+package org.apache.coyote.requests;
+
+import java.util.Arrays;
+import java.util.Map;
+
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toMap;
+
+public class RequestHeader {
+ private final Map items;
+
+ private RequestHeader(Map items) {
+ this.items = items;
+ }
+
+ public static RequestHeader from(String string) {
+ return Arrays.stream(string.split("\r\n"))
+ .map(line -> line.split(": "))
+ .collect(collectingAndThen(
+ toMap(line -> line[0], line -> line[1]),
+ RequestHeader::new
+ ));
+ }
+
+ public String get(String key) {
+ return items.get(key);
+ }
+}
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 @@