diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java
index e1b6cca042..f054af8a00 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.nio.file.Paths;
import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
/**
- * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다.
- * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
+ * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
*/
@DisplayName("File 클래스 학습 테스트")
class FileTest {
/**
* resource 디렉터리 경로 찾기
- *
- * File 객체를 생성하려면 파일의 경로를 알아야 한다.
- * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다.
- * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까?
+ *
+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource
+ * 디렉터리의 경로는 어떻게 알아낼 수 있을까?
*/
@Test
void resource_디렉터리에_있는_파일의_경로를_찾는다() {
final String fileName = "nextstep.txt";
// todo
- final String actual = "";
+ final URL resource = getClass().getClassLoader().getResource(fileName);
+ final String actual = resource.getFile();
assertThat(actual).endsWith(fileName);
}
/**
* 파일 내용 읽기
- *
- * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
- * File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
+ *
+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
- void 파일의_내용을_읽는다() {
+ void 파일의_내용을_읽는다() throws IOException {
final String fileName = "nextstep.txt";
// todo
- final Path path = null;
-
+ final URL resource = getClass().getClassLoader().getResource(fileName);
// todo
- final List actual = Collections.emptyList();
+ final List actual = Files.readAllLines(Paths.get(resource.getFile()));
assertThat(actual).containsOnly("nextstep");
}
diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java
index 47a79356b6..fc5d4d94c7 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -1,45 +1,50 @@
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 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에 연결될 수 있다.
+ * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
+ *
+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
- * Stream은 데이터를 바이트로 읽고 쓴다.
- * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
- * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
+ *
+ * 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,6 +58,7 @@ class OutputStream_학습_테스트 {
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/
+ outputStream.write(bytes);
final String actual = outputStream.toString();
@@ -61,13 +67,11 @@ class OutputStream_학습_테스트 {
}
/**
- * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
- * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
- * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
- * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
- * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
- * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
+ * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
+ *
+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을
+ * 전송한다. Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로
+ * 해제해야 한다.
*/
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
@@ -78,19 +82,23 @@ class OutputStream_학습_테스트 {
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/
+ outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
/**
- * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
- * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
+ * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가
+ * 발생한다.
*/
@Test
void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
final OutputStream outputStream = mock(OutputStream.class);
-
+ try (outputStream) {
+ } catch (IOException e) {
+ throw e;
+ }
/**
* todo
* try-with-resources를 사용한다.
@@ -103,21 +111,19 @@ 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 사이의 값으로 변환된다.
- * 그리고 Stream 끝에 도달하면 -1을 반환한다.
+ * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터
+ * 127 사이의 값으로 변환된다. 그리고 Stream 끝에 도달하면 -1을 반환한다.
*/
@Test
void InputStream은_데이터를_바이트로_읽는다() throws IOException {
@@ -128,7 +134,7 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
- final String actual = "";
+ final String actual = new String(inputStream.readAllBytes());
assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
@@ -136,8 +142,8 @@ class InputStream_학습_테스트 {
}
/**
- * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
- * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
+ * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가
+ * 발생한다.
*/
@Test
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
@@ -148,6 +154,9 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try (inputStream) {
+
+ }
verify(inputStream, atLeastOnce()).close();
}
@@ -155,26 +164,23 @@ 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 객체를 생성하고 필터 생성자에 전달하면
+ * 필터에 연결된다. 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? DEFAULT_BUFFER_SIZE = 8192;
*/
@Test
- void 필터인_BufferedInputStream를_사용해보자() {
+ void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
- final InputStream bufferedInputStream = null;
-
- final byte[] actual = new byte[0];
+ final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
+ final byte[] actual = bufferedInputStream.readAllBytes();
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
@@ -182,31 +188,33 @@ class FilterStream_학습_테스트 {
}
/**
- * 자바의 기본 문자열은 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 StringBuilder actual = new StringBuilder();
-
+ final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
+ final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
+ final BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
+ while (bufferedReader.ready()) {
+ actual.append(bufferedReader.readLine());
+ actual.append("\r\n");
+ }
assertThat(actual).hasToString(emoji);
}
}
diff --git a/tomcat/src/main/java/common/FileReader.java b/tomcat/src/main/java/common/FileReader.java
new file mode 100644
index 0000000000..48f8cbe58d
--- /dev/null
+++ b/tomcat/src/main/java/common/FileReader.java
@@ -0,0 +1,37 @@
+package common;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileReader {
+
+ private static final String STATIC_RESOURCE_PATH = "static";
+ private static final String EXTENSION_HTML = ".html";
+
+ private FileReader() {
+ }
+
+ public static String readFile(String uri) throws IOException {
+ URL resource = findResource(uri);
+ Path path = Paths.get(resource.getFile());
+ return new String(Files.readAllBytes(path));
+ }
+
+ private static URL findResource(String fileName) {
+ URL resource = FileReader.class.getClassLoader()
+ .getResource(STATIC_RESOURCE_PATH + fileName);
+ if (resource == null) {
+ fileName = fileName + EXTENSION_HTML;
+ resource = FileReader.class.getClassLoader()
+ .getResource(STATIC_RESOURCE_PATH + fileName);
+ }
+ if (resource == null) {
+ return FileReader.class.getClassLoader()
+ .getResource(STATIC_RESOURCE_PATH + "/404.html");
+ }
+ return resource;
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/mvc/ModelAndView.java b/tomcat/src/main/java/nextstep/mvc/ModelAndView.java
new file mode 100644
index 0000000000..4c93cd00ef
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/mvc/ModelAndView.java
@@ -0,0 +1,30 @@
+package nextstep.mvc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ModelAndView {
+
+ private final String viewName;
+ private final Map model = new HashMap<>();
+
+ public ModelAndView(final String viewName) {
+ this.viewName = viewName;
+ }
+
+ public ModelAndView() {
+ this(null);
+ }
+
+ public void setAttribute(String key, String value) {
+ model.put(key, value);
+ }
+
+ public String getViewName() {
+ return viewName;
+ }
+
+ public Map getModel() {
+ return model;
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/mvc/View.java b/tomcat/src/main/java/nextstep/mvc/View.java
new file mode 100644
index 0000000000..2be22ad97c
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/mvc/View.java
@@ -0,0 +1,41 @@
+package nextstep.mvc;
+
+import java.util.Map;
+import org.apache.coyote.http11.HttpHeaders;
+import org.apache.coyote.http11.HttpResponse;
+
+public class View {
+
+ private static final String DEFAULT_CHAR_SET = "text/html;charset=utf-8";
+
+ private String content;
+ private final String contentType;
+
+ public View() {
+ this.contentType = DEFAULT_CHAR_SET;
+ }
+
+ public View(final String content, final String contentType) {
+ this.content = content;
+ this.contentType = contentType;
+ }
+
+ public void render(Map model, HttpResponse httpResponse) {
+ if (content == null) {
+ StringBuilder sb = new StringBuilder();
+ for (final var value : model.values()) {
+ sb.append(value);
+ }
+ httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, DEFAULT_CHAR_SET);
+ httpResponse.addHeader(HttpHeaders.CONTENT_LENGTH,
+ String.valueOf(sb.toString().getBytes().length));
+ httpResponse.setBody(sb.toString());
+ return;
+ }
+
+ httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, contentType);
+ httpResponse.addHeader(HttpHeaders.CONTENT_LENGTH,
+ String.valueOf(content.getBytes().length));
+ httpResponse.setBody(content);
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/mvc/ViewResolver.java b/tomcat/src/main/java/nextstep/mvc/ViewResolver.java
new file mode 100644
index 0000000000..67cbf05ce5
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/mvc/ViewResolver.java
@@ -0,0 +1,15 @@
+package nextstep.mvc;
+
+import common.FileReader;
+import java.io.IOException;
+import org.apache.coyote.http11.SupportContentType;
+
+public class ViewResolver {
+
+ private ViewResolver() {
+ }
+
+ public static View resolve(final String viewName) throws IOException {
+ return new View(FileReader.readFile(viewName), SupportContentType.getContentType(viewName));
+ }
+}
diff --git a/tomcat/src/main/java/nextstep/mvc/handler/Handler.java b/tomcat/src/main/java/nextstep/mvc/handler/Handler.java
new file mode 100644
index 0000000000..3d2a8a9433
--- /dev/null
+++ b/tomcat/src/main/java/nextstep/mvc/handler/Handler.java
@@ -0,0 +1,159 @@
+package nextstep.mvc.handler;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+import nextstep.mvc.ModelAndView;
+import nextstep.mvc.View;
+import nextstep.mvc.ViewResolver;
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.catalina.manager.SessionManager;
+import org.apache.coyote.http11.HttpCookie;
+import org.apache.coyote.http11.HttpHeaders;
+import org.apache.coyote.http11.HttpMethod;
+import org.apache.coyote.http11.HttpRequest;
+import org.apache.coyote.http11.HttpResponse;
+import org.apache.coyote.http11.HttpStatus;
+import org.apache.coyote.http11.Session;
+import org.apache.coyote.http11.util.HttpParser;
+
+public class Handler {
+
+ private static final String INDEX_HTML = "/index.html";
+ private static final String BAD_REQUEST_HTML = "/400.html";
+ private static final String UNAUTHORIZED_HTML = "/401.html";
+
+ private Handler() {
+ }
+
+ public static void handle(HttpRequest httpRequest, HttpResponse httpResponse)
+ throws IOException {
+ ModelAndView modelAndView = handler(httpRequest, httpResponse);
+ View view = new View();
+ if (modelAndView.getViewName() != null) {
+ view = ViewResolver.resolve(modelAndView.getViewName());
+ }
+ view.render(modelAndView.getModel(), httpResponse);
+ }
+
+ private static ModelAndView handler(final HttpRequest httpRequest,
+ final HttpResponse httpResponse) {
+ final var path = httpRequest.getPath();
+ HttpMethod method = httpRequest.getHttpMethod();
+ if (method == HttpMethod.GET && path.equals("/")) {
+ httpResponse.setHttpStatus(HttpStatus.OK);
+ ModelAndView modelAndView = new ModelAndView();
+ modelAndView.setAttribute("message", "Hello world!");
+ return modelAndView;
+ }
+ if (method == HttpMethod.GET && path.isEmpty()) {
+ httpResponse.setHttpStatus(HttpStatus.OK);
+ return new ModelAndView(INDEX_HTML);
+ }
+ if (method == HttpMethod.GET && (path.equals("/login") || path.equals("/login.html"))) {
+ if (isAlreadyLogin(httpRequest)) {
+ httpResponse.setHttpStatus(HttpStatus.FOUND);
+ httpResponse.addHeader(HttpHeaders.LOCATION, INDEX_HTML);
+ return new ModelAndView(INDEX_HTML);
+ }
+ httpResponse.setHttpStatus(HttpStatus.OK);
+ return new ModelAndView(httpRequest.getPath());
+ }
+ if (method == HttpMethod.GET && path.endsWith(".html")) {
+ httpResponse.setHttpStatus(HttpStatus.OK);
+ return new ModelAndView(httpRequest.getPath());
+ }
+ if (method == HttpMethod.GET) {
+ httpResponse.setHttpStatus(HttpStatus.OK);
+ return new ModelAndView(httpRequest.getPath());
+ }
+ if (method == HttpMethod.POST && (path.equals("/login") || path.equals("/login.html"))) {
+ final var viewName = login(httpRequest, httpResponse);
+ return new ModelAndView(viewName);
+ }
+ if (method == HttpMethod.POST && (path.equals("/register") || path.equals(
+ "/register.html"))) {
+ final var viewName = register(httpRequest, httpResponse);
+ return new ModelAndView(viewName);
+ }
+ httpResponse.setHttpStatus(HttpStatus.BAD_REQUEST);
+ return new ModelAndView(BAD_REQUEST_HTML);
+ }
+
+ private static boolean isAlreadyLogin(final HttpRequest httpRequest) {
+ Session session = httpRequest.getSession();
+ if (session == null) {
+ return false;
+ }
+ return SessionManager.findSession(session.getId()).isPresent();
+ }
+
+ private static String login(HttpRequest httpRequest, HttpResponse httpResponse) {
+ final var body = httpRequest.getBody();
+ Map parameters = HttpParser.parseFormData(body);
+ if (parameters.containsKey("account") && parameters.containsKey("password")) {
+ return checkUser(httpResponse, parameters.get("account"), parameters.get("password"));
+ }
+ httpResponse.setHttpStatus(HttpStatus.BAD_REQUEST);
+ httpResponse.addHeader(HttpHeaders.LOCATION, BAD_REQUEST_HTML);
+ return BAD_REQUEST_HTML;
+ }
+
+ private static String checkUser(HttpResponse httpResponse, String account, String password) {
+ return InMemoryUserRepository.findByAccount(account)
+ .filter(user -> user.checkPassword(password))
+ .map(user -> loginSuccess(httpResponse, user))
+ .orElseGet(() -> loginFailed(httpResponse));
+ }
+
+ private static String loginSuccess(HttpResponse httpResponse, User user) {
+ httpResponse.setHttpStatus(HttpStatus.FOUND);
+ httpResponse.addHeader(HttpHeaders.LOCATION, INDEX_HTML);
+ final var session = new Session(UUID.randomUUID().toString());
+ session.setAttribute("user", user);
+ SessionManager.add(session);
+ httpResponse.addCookie(new HttpCookie(HttpCookie.JSESSIONID, session.getId()));
+ return INDEX_HTML;
+ }
+
+ private static String loginFailed(HttpResponse httpResponse) {
+ httpResponse.setHttpStatus(HttpStatus.UNAUTHORIZED);
+ httpResponse.addHeader(HttpHeaders.LOCATION, UNAUTHORIZED_HTML);
+ return UNAUTHORIZED_HTML;
+ }
+
+ private static String register(HttpRequest httpRequest, HttpResponse httpResponse) {
+ final String account = "account";
+ final String password = "password";
+ final String email = "email";
+
+ final var body = httpRequest.getBody();
+ Map parameters = HttpParser.parseFormData(body);
+ if (parameters.containsKey(account) &&
+ parameters.containsKey(password) &&
+ parameters.containsKey(email)
+ ) {
+ if (InMemoryUserRepository.findByAccount(parameters.get(account)).isPresent()) {
+ httpResponse.setHttpStatus(HttpStatus.BAD_REQUEST);
+ httpResponse.addHeader(HttpHeaders.LOCATION, BAD_REQUEST_HTML);
+ return BAD_REQUEST_HTML;
+ }
+ InMemoryUserRepository.save(
+ new User(
+ parameters.get(account),
+ parameters.get(password),
+ parameters.get(email)
+ )
+ );
+
+ httpResponse.setHttpStatus(HttpStatus.FOUND);
+ httpResponse.addHeader(HttpHeaders.LOCATION, INDEX_HTML);
+ return INDEX_HTML;
+ }
+
+ httpResponse.setHttpStatus(HttpStatus.BAD_REQUEST);
+ httpResponse.addHeader(HttpHeaders.LOCATION, BAD_REQUEST_HTML);
+ return BAD_REQUEST_HTML;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java
new file mode 100644
index 0000000000..0f99b27f1d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/catalina/manager/SessionManager.java
@@ -0,0 +1,30 @@
+package org.apache.catalina.manager;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.coyote.http11.Session;
+
+public class SessionManager {
+
+ private static final Map SESSIONS = new HashMap<>();
+
+ private SessionManager() {
+ }
+
+ public static void add(Session session) {
+ SESSIONS.put(session.getId(), session);
+ }
+
+ public static void remove(Session session) {
+ SESSIONS.remove(session.getId());
+ }
+
+ public static Optional findSession(String id) {
+ try {
+ return Optional.of(SESSIONS.get(id));
+ } catch (NullPointerException e) {
+ return Optional.empty();
+ }
+ }
+}
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..04ce70f50c 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,17 @@
package org.apache.coyote.http11;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import nextstep.mvc.handler.Handler;
import nextstep.jwp.exception.UncheckedServletException;
import org.apache.coyote.Processor;
+import org.apache.coyote.http11.util.HttpRequestReader;
+import org.apache.coyote.http11.util.HttpResponseWriter;
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);
@@ -26,20 +30,15 @@ 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 var inputStream = connection.getInputStream();
+ final var bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ final var outputStream = connection.getOutputStream()
+ ) {
+ HttpRequest httpRequest = HttpRequestReader.read(bufferedReader);
+ final var httpResponse = new HttpResponse();
+ Handler.handle(httpRequest, httpResponse);
+ HttpResponseWriter.write(outputStream, httpResponse);
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java
new file mode 100644
index 0000000000..7a8d58c0ff
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java
@@ -0,0 +1,22 @@
+package org.apache.coyote.http11;
+
+public class HttpCookie {
+
+ public static final String JSESSIONID = "JSESSIONID";
+
+ private final String name;
+ private final String value;
+
+ public HttpCookie(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java
new file mode 100644
index 0000000000..2f8b22dc10
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java
@@ -0,0 +1,43 @@
+package org.apache.coyote.http11;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class HttpHeaders {
+
+ public static final String CONTENT_LENGTH = "Content-Length";
+ public static final String CONTENT_TYPE = "Content-Type";
+ public static final String LOCATION = "Location";
+ public static final String SET_COOKIE = "Set-Cookie";
+ public static final String COOKIE = "Cookie";
+
+ private final Map headers;
+
+ public HttpHeaders() {
+ this.headers = new LinkedHashMap<>();
+ }
+
+ public HttpHeaders(final Map headers) {
+ this.headers = headers;
+ }
+
+ public boolean containsHeader(String headerName) {
+ return this.headers.containsKey(headerName);
+ }
+
+ public void put(String key, String value) {
+ headers.put(key, value);
+ }
+
+ public String get(String headerName) {
+ return headers.get(headerName);
+ }
+
+ public void putAll(final Map headers) {
+ this.headers.putAll(headers);
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java
new file mode 100644
index 0000000000..7ad5a1709b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpMethod.java
@@ -0,0 +1,6 @@
+package org.apache.coyote.http11;
+
+public enum HttpMethod {
+ GET,
+ POST
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java
new file mode 100644
index 0000000000..d403a1d3df
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java
@@ -0,0 +1,153 @@
+package org.apache.coyote.http11;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.coyote.http11.util.HttpParser;
+
+public class HttpRequest {
+
+ private final String protocol;
+ private final HttpHeaders headers;
+ private final Map cookies;
+ private final HttpMethod httpMethod;
+ private final String uri;
+ private final String path;
+ private final Map parameters;
+ private final Session session;
+ private final String body;
+
+ private HttpRequest(final String protocol,
+ final HttpHeaders headers,
+ final HttpMethod httpMethod,
+ final String uri, final String path, final Map parameters,
+ final String body) {
+ this.protocol = protocol;
+ this.headers = headers;
+ this.httpMethod = httpMethod;
+ this.uri = uri;
+ this.path = path;
+ this.parameters = parameters;
+ this.body = body;
+ this.cookies = getCookiesFromHeader();
+ this.session = getSessionFromHeader();
+ }
+
+ private Map getCookiesFromHeader() {
+ if (!this.headers.containsHeader(HttpHeaders.COOKIE)) {
+ return Collections.emptyMap();
+ }
+ List parsedCookies = HttpParser.parseCookies(
+ this.headers.get(HttpHeaders.COOKIE));
+ return parsedCookies.stream()
+ .collect(Collectors.toMap(HttpCookie::getName, Function.identity()));
+ }
+
+ private Session getSessionFromHeader() {
+ if (!this.cookies.containsKey(HttpCookie.JSESSIONID)) {
+ return null;
+ }
+ final var sessionCookie = this.cookies.get(HttpCookie.JSESSIONID);
+ return new Session(sessionCookie.getValue());
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public HttpCookie getCookie(String name) {
+ return cookies.get(name);
+ }
+
+ public Session getSession() {
+ return this.session;
+ }
+
+ public HttpMethod getHttpMethod() {
+ return httpMethod;
+ }
+
+ public String getHeader(String headerName) {
+ return this.headers.get(headerName);
+ }
+
+ public boolean containsHeader(String headerName) {
+ return this.headers.containsHeader(headerName);
+ }
+
+ public void addParameters(Map parameters) {
+ this.parameters.putAll(parameters);
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public static class Builder {
+
+ private String protocol;
+ private HttpHeaders headers;
+ private HttpMethod httpMethod;
+ private String uri;
+ private String path;
+ private Map parameters;
+ private String body;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public Builder protocol(final String protocol) {
+ this.protocol = protocol;
+ return this;
+ }
+
+ public Builder httpMethod(final HttpMethod httpMethod) {
+ this.httpMethod = httpMethod;
+ return this;
+ }
+
+ public Builder headers(final HttpHeaders headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder uri(final String uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ public Builder path(final String path) {
+ this.path = path;
+ return this;
+ }
+
+ public Builder parameters(final Map parameters) {
+ this.parameters = parameters;
+ return this;
+ }
+
+ public Builder body(final String body) {
+ this.body = body;
+ return this;
+ }
+
+ public HttpRequest build() {
+ return new HttpRequest(
+ protocol,
+ headers,
+ httpMethod,
+ uri,
+ path,
+ parameters,
+ body
+ );
+ }
+ }
+}
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..ff85529e57
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java
@@ -0,0 +1,63 @@
+package org.apache.coyote.http11;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpResponse {
+
+ private static final String DEFAULT_PROTOCOL = "HTTP/1.1";
+
+ private final HttpHeaders headers;
+ private final Map cookies = new HashMap<>();
+ private HttpStatus httpStatus;
+ private String body;
+
+ public HttpResponse() {
+ this.headers = new HttpHeaders();
+ }
+
+ public String getProtocol() {
+ return DEFAULT_PROTOCOL;
+ }
+
+ public HttpStatus getHttpStatus() {
+ return httpStatus;
+ }
+
+ public void setHttpStatus(HttpStatus httpStatus) {
+ this.httpStatus = httpStatus;
+ }
+
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+
+ public String getHeader(String headerName) {
+ return this.headers.get(headerName);
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(body.getBytes().length));
+ }
+
+ public void addHeader(String headerName, String value) {
+ this.headers.put(headerName, value);
+ }
+
+ public void addCookie(HttpCookie cookie) {
+ this.cookies.put(cookie.getName(), cookie);
+ }
+
+ public HttpCookie getCookie(String name) {
+ return this.cookies.get(name);
+ }
+
+ public Map getCookies() {
+ return this.cookies;
+ }
+}
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..11485b506d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java
@@ -0,0 +1,43 @@
+package org.apache.coyote.http11;
+
+public enum HttpStatus {
+ OK(200, "OK"),
+ FOUND(302, "Found"),
+ BAD_REQUEST(400, "Bad Request"),
+ UNAUTHORIZED(401, "Unauthorized"),
+ NOT_FOUND(404, "Not Found"),
+ INTER_SERVER_ERROR(500, "Internal Server Error");
+
+ private final int value;
+ private final String reasonPhrase;
+
+ HttpStatus(int value, String reasonPhrase) {
+ this.value = value;
+ this.reasonPhrase = reasonPhrase;
+ }
+
+ public static HttpStatus valueOf(int statusCode) {
+ HttpStatus status = resolve(statusCode);
+ if (status == null) {
+ throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
+ }
+ return null;
+ }
+
+ private static HttpStatus resolve(int statusCode) {
+ for (HttpStatus status : values()) {
+ if (status.value == statusCode) {
+ return status;
+ }
+ }
+ return null;
+ }
+
+ public String getReasonPhrase() {
+ return this.reasonPhrase;
+ }
+
+ public int getValue() {
+ return value;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/Session.java
new file mode 100644
index 0000000000..72aae4610c
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Session.java
@@ -0,0 +1,22 @@
+package org.apache.coyote.http11;
+
+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 id;
+ }
+
+ public void setAttribute(final String name, final Object value) {
+ this.values.put(name, value);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/SupportContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/SupportContentType.java
new file mode 100644
index 0000000000..9be15347f6
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/SupportContentType.java
@@ -0,0 +1,26 @@
+package org.apache.coyote.http11;
+
+public enum SupportContentType {
+ TEXT_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 final String endWith;
+ private final String contentType;
+
+ SupportContentType(final String endWith, final String contentType) {
+ this.endWith = endWith;
+ this.contentType = contentType;
+ }
+
+ public static String getContentType(String fileName) {
+ for (final SupportContentType value : values()) {
+ if (fileName.endsWith(value.endWith)) {
+ return value.contentType;
+ }
+ }
+ return TEXT_HTML.contentType;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpFormException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpFormException.java
new file mode 100644
index 0000000000..6fb7b081dd
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/InvalidHttpFormException.java
@@ -0,0 +1,5 @@
+package org.apache.coyote.http11.exception;
+
+public class InvalidHttpFormException extends RuntimeException {
+
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/util/HttpParser.java b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpParser.java
new file mode 100644
index 0000000000..7ab0e7bddd
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpParser.java
@@ -0,0 +1,56 @@
+package org.apache.coyote.http11.util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.coyote.http11.HttpCookie;
+
+public class HttpParser {
+
+ private static final String QUERY_STRING_START_SYMBOL = "?";
+ private static final String PARAMETER_SEPARATOR = "&";
+ private static final String KEY_VALUE_DELIMITER = "=";
+ private static final String COOKIE_SEPARATOR = "; ";
+
+ private HttpParser() {
+ }
+
+ public static Map parseQueryParameters(String uri) {
+ if (uri.contains(QUERY_STRING_START_SYMBOL)) {
+ final var queryStartIndex = uri.indexOf(QUERY_STRING_START_SYMBOL);
+ final var queryString = uri.substring(queryStartIndex + 1);
+ return parseParameters(queryString);
+ }
+ return Collections.emptyMap();
+ }
+
+ private static Map parseParameters(String queryString) {
+ return Arrays.stream(queryString.split(PARAMETER_SEPARATOR))
+ .map(parameter -> parameter.split(KEY_VALUE_DELIMITER))
+ .filter(keyValuePair -> keyValuePair.length == 2)
+ .collect(Collectors.toMap(keyValuePair -> keyValuePair[0],
+ keyValuePair -> keyValuePair[1]));
+ }
+
+ public static String parsePath(String uri) {
+ return uri.split("\\?")[0];
+ }
+
+ public static List parseCookies(String cookieLine) {
+ final var cookies = cookieLine.split(COOKIE_SEPARATOR);
+ return Arrays.stream(cookies)
+ .map(cookie -> cookie.split(KEY_VALUE_DELIMITER))
+ .map(pair -> new HttpCookie(pair[0], pair[1]))
+ .collect(Collectors.toList());
+ }
+
+ public static Map parseFormData(String formData) {
+ return Arrays.stream(formData.split(PARAMETER_SEPARATOR))
+ .map(parameter -> parameter.split(KEY_VALUE_DELIMITER))
+ .filter(keyValuePair -> keyValuePair.length == 2)
+ .collect(Collectors.toMap(keyValuePair -> keyValuePair[0],
+ keyValuePair -> keyValuePair[1]));
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/util/HttpRequestReader.java b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpRequestReader.java
new file mode 100644
index 0000000000..45311607ac
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpRequestReader.java
@@ -0,0 +1,64 @@
+package org.apache.coyote.http11.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.apache.coyote.http11.HttpHeaders;
+import org.apache.coyote.http11.HttpMethod;
+import org.apache.coyote.http11.HttpRequest;
+import org.apache.coyote.http11.exception.InvalidHttpFormException;
+
+public class HttpRequestReader {
+
+ private static final String REQUEST_LINE_DELIMITER = " ";
+ private static final String REQUEST_HEADER_DELIMITER = ": ";
+
+ private HttpRequestReader() {
+ }
+
+ public static HttpRequest read(final BufferedReader bufferedReader) throws IOException {
+ final var requestLine = bufferedReader.readLine();
+ if (requestLine == null) {
+ throw new InvalidHttpFormException();
+ }
+ final var requestLineAttributes = requestLine.split(REQUEST_LINE_DELIMITER);
+ Map headers = readRequestHeaders(bufferedReader);
+ String body = readRequestBody(headers, bufferedReader);
+ return HttpRequest.Builder.builder()
+ .httpMethod(HttpMethod.valueOf(requestLineAttributes[0]))
+ .uri(requestLineAttributes[1])
+ .path(HttpParser.parsePath(requestLineAttributes[1]))
+ .parameters(HttpParser.parseQueryParameters(requestLineAttributes[1]))
+ .protocol(requestLineAttributes[2])
+ .headers(new HttpHeaders(headers))
+ .body(body)
+ .build();
+ }
+
+ private static Map readRequestHeaders(BufferedReader bufferedReader)
+ throws IOException {
+ final Map headers = new LinkedHashMap<>();
+ while (bufferedReader.ready()) {
+ final var header = bufferedReader.readLine();
+ if (header.isEmpty()) {
+ break;
+ }
+ final var keyValuePair = header.split(REQUEST_HEADER_DELIMITER);
+ headers.put(keyValuePair[0], keyValuePair[1]);
+ }
+ return headers;
+ }
+
+ private static String readRequestBody(Map headers,
+ BufferedReader bufferedReader)
+ throws IOException {
+ if (!headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
+ return null;
+ }
+ int contentLength = Integer.parseInt(headers.get(HttpHeaders.CONTENT_LENGTH));
+ char[] buffer = new char[contentLength];
+ bufferedReader.read(buffer, 0, contentLength);
+ return new String(buffer);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/util/HttpResponseWriter.java b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpResponseWriter.java
new file mode 100644
index 0000000000..4439fc626b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/util/HttpResponseWriter.java
@@ -0,0 +1,54 @@
+package org.apache.coyote.http11.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map.Entry;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+import org.apache.coyote.http11.HttpCookie;
+import org.apache.coyote.http11.HttpHeaders;
+import org.apache.coyote.http11.HttpResponse;
+
+public class HttpResponseWriter {
+
+ private static final String HEADER_DELIMITER = ": ";
+
+ private HttpResponseWriter() {
+ }
+
+ public static void write(OutputStream outputStream, HttpResponse httpResponse)
+ throws IOException {
+
+ StringJoiner stringJoiner = new StringJoiner(" \r\n");
+ stringJoiner.add(createStatusLine(httpResponse));
+ HttpHeaders headers = httpResponse.getHeaders();
+ for (final Entry entry : headers.getHeaders().entrySet()) {
+ stringJoiner.add(String.join(HEADER_DELIMITER, entry.getKey(), entry.getValue()));
+ }
+ String cookieLine = getCookieLine(httpResponse);
+ if (!cookieLine.isEmpty()) {
+ stringJoiner.add(cookieLine);
+ }
+ stringJoiner.add("\r\n" + httpResponse.getBody());
+
+ String response = stringJoiner.toString();
+ outputStream.write(response.getBytes());
+ outputStream.flush();
+ }
+
+ private static String createStatusLine(HttpResponse httpResponse) {
+ return String.join(" ", httpResponse.getProtocol(),
+ String.valueOf(httpResponse.getHttpStatus().getValue()),
+ httpResponse.getHttpStatus().getReasonPhrase());
+ }
+
+ private static String getCookieLine(HttpResponse httpResponse) {
+ if (httpResponse.getCookie(HttpCookie.JSESSIONID) == null) {
+ return "";
+ }
+ String cookieValues = httpResponse.getCookies().values().stream()
+ .map(cookie -> cookie.getName() + "=" + cookie.getValue())
+ .collect(Collectors.joining("; "));
+ return String.join(HEADER_DELIMITER, HttpHeaders.SET_COOKIE, cookieValues);
+ }
+}
diff --git a/tomcat/src/main/resources/static/400.html b/tomcat/src/main/resources/static/400.html
new file mode 100644
index 0000000000..a0095232e9
--- /dev/null
+++ b/tomcat/src/main/resources/static/400.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+ 400 Error - SB Admin
+
+
+
+
+