diff --git a/README.md b/README.md
index b24f542e33..a6b9d0453e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,25 @@
-# 톰캣 구현하기
+# 톰캣 구현하기
+
+
+### html 파일 응답하기
+* [x] HTTP Request Header에서 필요한 정보를 파싱한다.
+ * [x] HTTP 메소드를 저장한다.
+ * [x] Request URI(html 파일)를 저장한다.
+ * [x] HTTP 버전을 저장한다.
+* [x] Request URI에 해당하는 파일을 responseBody로 돌려준다.
+
+### CSS 지원하기
+* [x] 요청 리소스가 CSS or JS인 경우 Response Header에 적절한 Content-Type을 보낸다.
+
+### 로그인
+* [x] 로그인 페이지를 응답한다.
+ * [x] Request URI에서 QueryString을 파싱해 저장한다.
+ * [x] QueryString에서 유저의 정보를 추출해 콘솔로 출력한다.
+ * [x] 세션 정보가 있다면 index 페이지로 리다이렉트 한다.
+* [x] 로그인에 성공하면 index 페이지로 리다이렉트한다.
+ * [x] Cookie에 JSESSIONID가 없다면 추가한다.
+* [x] 로그인에 실패하면 401 페이지로 리다이렉트한다.
+
+### 회원가입
+* [x] 회원가입 페이지를 응답한다.
+* [x] 회원가입을 수행하면 index 페이지로 리다이렉트한다.
diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java
index e1b6cca042..5f6304fa40 100644
--- a/study/src/test/java/study/FileTest.java
+++ b/study/src/test/java/study/FileTest.java
@@ -1,53 +1,51 @@
package study;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Collections;
import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
+import 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_디렉터리에_있는_파일의_경로를_찾는다() {
+ void resource_디렉터리에_있는_파일의_경로를_찾는다() throws IOException {
final String fileName = "nextstep.txt";
// todo
- final String actual = "";
+ final URL resource = getClass().getClassLoader().getResource(fileName);
+ assert resource != null;
+ final String actual = resource.getPath();
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;
-
- // todo
- final List actual = Collections.emptyList();
+ final URL resource = getClass().getClassLoader().getResource(fileName);
+ final Path path = new File(resource.getPath()).toPath();
+ final List actual = Files.readAllLines(path);
assertThat(actual).containsOnly("nextstep");
}
diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java
index 47a79356b6..88771c0a78 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -1,45 +1,51 @@
package study;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import java.io.*;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
-
/**
- * 자바는 스트림(Stream)으로부터 I/O를 사용한다.
- * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
- *
- * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
- * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
- * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
- * Stream은 데이터를 바이트로 읽고 쓴다.
- * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
- * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
+ * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
+ *
+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는
+ * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
+ *
+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을
+ * 처리할 수 있다.
*/
@DisplayName("Java I/O Stream 클래스 학습 테스트")
class IOStreamTest {
/**
* OutputStream 학습하기
- *
- * 자바의 기본 출력 클래스는 java.io.OutputStream이다.
- * OutputStream의 write(int b) 메서드는 기반 메서드이다.
+ *
+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다.
* public abstract void write(int b) throws IOException;
*/
@Nested
class OutputStream_학습_테스트 {
/**
- * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다.
- * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
- * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
- * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
- *
+ * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를
+ * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때
+ * 사용한다.
+ *
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
* write(byte[] data)와 write(byte b[], int off, int len) 메서드는
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
@@ -53,6 +59,7 @@ class OutputStream_학습_테스트 {
* todo
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/
+ outputStream.write(bytes);
final String actual = outputStream.toString();
@@ -61,13 +68,10 @@ class OutputStream_학습_테스트 {
}
/**
- * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
- * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
- * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
- * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
- * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
- * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
+ * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
+ *
+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은
+ * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다.
*/
@Test
void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException {
@@ -78,24 +82,26 @@ 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);
-
+ final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
/**
* todo
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try (outputStream) {
+ }
verify(outputStream, atLeastOnce()).close();
}
@@ -103,20 +109,18 @@ class OutputStream_학습_테스트 {
/**
* InputStream 학습하기
- *
- * 자바의 기본 입력 클래스는 java.io.InputStream이다.
- * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
- * InputStream의 read() 메서드는 기반 메서드이다.
+ *
+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반
+ * 메서드이다.
* public abstract int read() throws IOException;
- *
+ *
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
class InputStream_학습_테스트 {
/**
- * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다.
- * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
+ * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다.
* 그리고 Stream 끝에 도달하면 -1을 반환한다.
*/
@Test
@@ -128,7 +132,8 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
- final String actual = "";
+
+ final String actual = new String(inputStream.readAllBytes());
assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
@@ -136,8 +141,7 @@ class InputStream_학습_테스트 {
}
/**
- * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다.
- * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
+ * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다.
*/
@Test
void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException {
@@ -148,33 +152,33 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
+ try (inputStream) {
+ }
verify(inputStream, atLeastOnce()).close();
}
}
/**
* FilterStream 학습하기
- *
- * 필터는 필터 스트림, reader, writer로 나뉜다.
- * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
- * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
+ *
+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된
+ * 텍스트를 처리하는 데 사용된다.
*/
@Nested
class FilterStream_학습_테스트 {
/**
- * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다.
- * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다.
- * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
+ * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지
+ * 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
- void 필터인_BufferedInputStream를_사용해보자() {
+ void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
- final InputStream bufferedInputStream = null;
+ final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
- final byte[] actual = new byte[0];
+ final byte[] actual = bufferedInputStream.readAllBytes();
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
@@ -182,30 +186,34 @@ 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());
+ InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+ BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
final StringBuilder actual = new StringBuilder();
+ String line = bufferedReader.readLine();
+ while (line != null) {
+ actual.append(line);
+ actual.append("\r\n");
+ line = bufferedReader.readLine();
+ }
assertThat(actual).hasToString(emoji);
}
diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java
index 1ca30e8383..8d1f3b5391 100644
--- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java
+++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java
@@ -1,10 +1,9 @@
package nextstep.jwp.db;
-import nextstep.jwp.model.User;
-
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import nextstep.jwp.model.User;
public class InMemoryUserRepository {
@@ -23,5 +22,14 @@ public static Optional findByAccount(String account) {
return Optional.ofNullable(database.get(account));
}
- private InMemoryUserRepository() {}
+ public static Optional findByAccountAndPassword(final String account, final String password) {
+ final Optional user = Optional.ofNullable(database.get(account));
+ if (user.isPresent() && !user.get().checkPassword(password)) {
+ return Optional.empty();
+ }
+ return user;
+ }
+
+ private InMemoryUserRepository() {
+ }
}
diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java
index 3b2c4dda7c..cfd020ac8d 100644
--- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java
+++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java
@@ -1,95 +1,98 @@
-package org.apache.catalina.connector;
-
-import org.apache.coyote.http11.Http11Processor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.ServerSocket;
-import java.net.Socket;
-
-public class Connector implements Runnable {
-
- private static final Logger log = LoggerFactory.getLogger(Connector.class);
-
- private static final int DEFAULT_PORT = 8080;
- private static final int DEFAULT_ACCEPT_COUNT = 100;
-
- private final ServerSocket serverSocket;
- private boolean stopped;
-
- public Connector() {
- this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
- }
-
- public Connector(final int port, final int acceptCount) {
- this.serverSocket = createServerSocket(port, acceptCount);
- this.stopped = false;
- }
-
- private ServerSocket createServerSocket(final int port, final int acceptCount) {
- try {
- final int checkedPort = checkPort(port);
- final int checkedAcceptCount = checkAcceptCount(acceptCount);
- return new ServerSocket(checkedPort, checkedAcceptCount);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- public void start() {
- var thread = new Thread(this);
- thread.setDaemon(true);
- thread.start();
- stopped = false;
- log.info("Web Application Server started {} port.", serverSocket.getLocalPort());
- }
-
- @Override
- public void run() {
- // 클라이언트가 연결될때까지 대기한다.
- while (!stopped) {
- connect();
- }
- }
-
- private void connect() {
- try {
- process(serverSocket.accept());
- } catch (IOException e) {
- log.error(e.getMessage(), e);
- }
- }
-
- private void process(final Socket connection) {
- if (connection == null) {
- return;
- }
- var processor = new Http11Processor(connection);
- new Thread(processor).start();
- }
-
- public void stop() {
- stopped = true;
- try {
- serverSocket.close();
- } catch (IOException e) {
- log.error(e.getMessage(), e);
- }
- }
-
- private int checkPort(final int port) {
- final var MIN_PORT = 1;
- final var MAX_PORT = 65535;
-
- if (port < MIN_PORT || MAX_PORT < port) {
- return DEFAULT_PORT;
- }
- return port;
- }
-
- private int checkAcceptCount(final int acceptCount) {
- return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT);
- }
-}
+package org.apache.catalina.connector;
+
+import org.apache.coyote.http11.Http11Processor;
+import org.apache.coyote.http11.handler.HandlerMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+public class Connector implements Runnable {
+
+ private static final Logger log = LoggerFactory.getLogger(Connector.class);
+
+ private static final int DEFAULT_PORT = 8080;
+ private static final int DEFAULT_ACCEPT_COUNT = 100;
+
+ private final ServerSocket serverSocket;
+ private final HandlerMapper handlerMapper;
+ private boolean stopped;
+
+ public Connector(final HandlerMapper handlerMapper) {
+ this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, handlerMapper);
+ }
+
+ public Connector(final int port, final int acceptCount, final HandlerMapper handlerMapper) {
+ this.serverSocket = createServerSocket(port, acceptCount);
+ this.stopped = false;
+ this.handlerMapper = handlerMapper;
+ }
+
+ private ServerSocket createServerSocket(final int port, final int acceptCount) {
+ try {
+ final int checkedPort = checkPort(port);
+ final int checkedAcceptCount = checkAcceptCount(acceptCount);
+ return new ServerSocket(checkedPort, checkedAcceptCount);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ public void start() {
+ var thread = new Thread(this);
+ thread.setDaemon(true);
+ thread.start();
+ stopped = false;
+ log.info("Web Application Server started {} port.", serverSocket.getLocalPort());
+ }
+
+ @Override
+ public void run() {
+ // 클라이언트가 연결될때까지 대기한다.
+ while (!stopped) {
+ connect();
+ }
+ }
+
+ private void connect() {
+ try {
+ process(serverSocket.accept());
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ private void process(final Socket connection) {
+ if (connection == null) {
+ return;
+ }
+ var processor = new Http11Processor(connection, handlerMapper);
+ new Thread(processor).start();
+ }
+
+ public void stop() {
+ stopped = true;
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ private int checkPort(final int port) {
+ final var MIN_PORT = 1;
+ final var MAX_PORT = 65535;
+
+ if (port < MIN_PORT || MAX_PORT < port) {
+ return DEFAULT_PORT;
+ }
+ return port;
+ }
+
+ private int checkAcceptCount(final int acceptCount) {
+ return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java
index 205159e95b..4e6aeb80fe 100644
--- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java
+++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java
@@ -1,27 +1,30 @@
-package org.apache.catalina.startup;
-
-import org.apache.catalina.connector.Connector;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-public class Tomcat {
-
- private static final Logger log = LoggerFactory.getLogger(Tomcat.class);
-
- public void start() {
- var connector = new Connector();
- connector.start();
-
- try {
- // make the application wait until we press any key.
- System.in.read();
- } catch (IOException e) {
- log.error(e.getMessage(), e);
- } finally {
- log.info("web server stop.");
- connector.stop();
- }
- }
-}
+package org.apache.catalina.startup;
+
+import org.apache.catalina.connector.Connector;
+import org.apache.coyote.http11.handler.HandlerMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class Tomcat {
+
+ private static final Logger log = LoggerFactory.getLogger(Tomcat.class);
+
+ private final HandlerMapper handlerMapper = new HandlerMapper();
+
+ public void start() {
+ var connector = new Connector(handlerMapper);
+ connector.start();
+
+ try {
+ // make the application wait until we press any key.
+ System.in.read();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ log.info("web server stop.");
+ connector.stop();
+ }
+ }
+}
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..e2626c7b5a 100644
--- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
@@ -1,47 +1,50 @@
-package org.apache.coyote.http11;
-
-import nextstep.jwp.exception.UncheckedServletException;
-import org.apache.coyote.Processor;
-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;
-
- public Http11Processor(final Socket connection) {
- this.connection = connection;
- }
-
- @Override
- public void run() {
- log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
- process(connection);
- }
-
- @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();
- } catch (IOException | UncheckedServletException e) {
- log.error(e.getMessage(), e);
- }
- }
-}
+package org.apache.coyote.http11;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.Socket;
+import nextstep.jwp.exception.UncheckedServletException;
+import org.apache.coyote.Processor;
+import org.apache.coyote.http11.handler.HandlerMapper;
+import org.apache.coyote.http11.message.request.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Http11Processor implements Runnable, Processor {
+
+ private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
+
+ private final Socket connection;
+
+ private final HandlerMapper handlerMapper;
+
+ public Http11Processor(final Socket connection, final HandlerMapper handlerMapper) {
+ this.connection = connection;
+ this.handlerMapper = handlerMapper;
+ }
+
+ @Override
+ public void run() {
+ log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
+ process(connection);
+ }
+
+ @Override
+ public void process(final Socket connection) {
+ try (final var inputStream = connection.getInputStream();
+ final var outputStream = connection.getOutputStream();
+ final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
+ final Request request = Request.from(bufferedReader);
+ final String response = createResponse(request);
+ outputStream.write(response.getBytes());
+ outputStream.flush();
+ } catch (final IOException | UncheckedServletException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ private String createResponse(final Request request) {
+ return handlerMapper.handle(request).getResponse();
+ }
+}
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..26f2ad4723
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/Session.java
@@ -0,0 +1,34 @@
+package org.apache.coyote.http11;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public class Session {
+ private final String id;
+ private final Map values = new HashMap<>();
+
+ public Session() {
+ this.id = String.valueOf(UUID.randomUUID());
+ }
+
+ 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) {
+ values.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ values.remove(name);
+ }
+
+ public void invalidate() {
+ values.clear();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java
new file mode 100644
index 0000000000..a6840c995b
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SessionManager {
+ private static final Map SESSIONS = new HashMap<>();
+
+ public static void add(final Session session) {
+ SESSIONS.put(session.getId(), session);
+ }
+
+ public static Session findSession(final String id) {
+ return SESSIONS.get(id);
+ }
+
+ public static void remove(final String id) {
+ SESSIONS.remove(id);
+ }
+
+ private SessionManager() {
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java
new file mode 100644
index 0000000000..b029db6fa2
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java
@@ -0,0 +1,98 @@
+package org.apache.coyote.http11.handler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import nextstep.jwp.db.InMemoryUserRepository;
+import nextstep.jwp.model.User;
+import org.apache.coyote.http11.Session;
+import org.apache.coyote.http11.SessionManager;
+import org.apache.coyote.http11.message.HttpStatus;
+import org.apache.coyote.http11.message.request.Request;
+import org.apache.coyote.http11.message.request.RequestLine;
+import org.apache.coyote.http11.message.response.Response;
+
+public class HandlerMapper {
+ private final Map> handlers = new HashMap<>();
+
+ public HandlerMapper() {
+ init();
+ }
+
+ private void init() {
+ handlers.put(new HandlerStatus("GET", "/"), this::rootHandler);
+ handlers.put(new HandlerStatus("GET", "/login"), this::loginHandler);
+ handlers.put(new HandlerStatus("POST", "/login"), this::loginFormHandler);
+ handlers.put(new HandlerStatus("GET", "/register"), this::registerHandler);
+ handlers.put(new HandlerStatus("POST", "/register"), this::registerFormHandler);
+ }
+
+ public Response rootHandler(final Request request) {
+ return Response.createByResponseBody(HttpStatus.OK, "Hello world!");
+ }
+
+ public Response loginHandler(final Request request) {
+ if (request.getSessionValue("user") != Optional.empty()) {
+ return Response.createByTemplate(HttpStatus.FOUND, "index.html");
+ }
+
+ return Response.createByTemplate(HttpStatus.OK, "login.html");
+ }
+
+ public Response loginFormHandler(final Request request) {
+ final Map requestForms = request.getRequestForms().getFormData();
+ final Optional user = login(requestForms.get("account"), requestForms.get("password"));
+ return user.map(value -> loginSuccess(request, value)).orElseGet(this::loginFail);
+ }
+
+ private Response loginSuccess(final Request request, final User user) {
+ if (request.noSession()) {
+ final Session session = new Session();
+ session.setAttribute("user", user);
+ SessionManager.add(session);
+
+ final Map header = new HashMap<>();
+ header.put("Set-Cookie", "JSESSIONID=" + session.getId());
+ return Response.createByTemplate(HttpStatus.FOUND, "index.html", header);
+ }
+ return Response.createByTemplate(HttpStatus.FOUND, "index.html");
+ }
+
+ private Response loginFail() {
+ return Response.createByTemplate(HttpStatus.UNAUTHORIZED, "401.html");
+ }
+
+ private Optional login(final String account, final String password) {
+ return InMemoryUserRepository.findByAccountAndPassword(account, password);
+ }
+
+ public Response registerHandler(final Request request) {
+ return Response.createByTemplate(HttpStatus.OK, "register.html");
+ }
+
+ public Response registerFormHandler(final Request request) {
+ final Map requestForms = request.getRequestForms().getFormData();
+ final String account = requestForms.get("account");
+ final String email = requestForms.get("email");
+ final String password = requestForms.get("password");
+ InMemoryUserRepository.save(new User(account, password, email));
+
+ return Response.createByTemplate(HttpStatus.FOUND, "index.html");
+ }
+
+ public Response handle(final Request request) {
+ final RequestLine requestLine = request.getRequestLine();
+ final HandlerStatus handlerStatus = HandlerStatus.from(requestLine);
+
+ final Function handler = handlers.get(handlerStatus);
+ if (handler != null) {
+ return handler.apply(request);
+ }
+
+ if (requestLine.getRequestURI().isExistFile()) {
+ return Response.createByTemplate(request.getRequestLine().getRequestURI());
+ }
+ throw new IllegalArgumentException("매핑되는 핸들러가 존재하지 않습니다.");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java
new file mode 100644
index 0000000000..f59c0e4cc9
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java
@@ -0,0 +1,43 @@
+package org.apache.coyote.http11.handler;
+
+import java.util.Objects;
+import org.apache.coyote.http11.message.request.RequestLine;
+
+public class HandlerStatus {
+ private final String httpMethod;
+ private final String path;
+
+ public HandlerStatus(final String httpMethod, final String path) {
+ this.httpMethod = httpMethod;
+ this.path = path;
+ }
+
+ public static HandlerStatus from(final RequestLine requestLine) {
+ return new HandlerStatus(requestLine.getHttpMethod(), requestLine.getPath());
+ }
+
+ public String getHttpMethod() {
+ return httpMethod;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof HandlerStatus)) {
+ return false;
+ }
+ final HandlerStatus that = (HandlerStatus) o;
+ return Objects.equals(httpMethod, that.httpMethod) && Objects.equals(path, that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(httpMethod, path);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java
new file mode 100644
index 0000000000..f5ad90ef7c
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java
@@ -0,0 +1,46 @@
+package org.apache.coyote.http11.message;
+
+import java.util.Arrays;
+
+public enum ContentType {
+ HTML(".html", "text/html"),
+ CSS(".css", "text/css"),
+ JS(".js", "text/javascript"),
+ ICO(".ico", "image/x-icon");
+
+ private final String extension;
+ private final String headerValue;
+
+ ContentType(final String extension, final String headerValue) {
+ this.extension = extension;
+ this.headerValue = headerValue;
+ }
+
+ public static ContentType findByFileName(final String fileName) {
+ final String extension = getExtension(fileName);
+ return findByExtension(extension);
+ }
+
+ public static String getExtension(final String fileName) {
+ final int lastDotIndex = fileName.lastIndexOf('.');
+ if (lastDotIndex > 0) {
+ return fileName.substring(lastDotIndex);
+ }
+ throw new IllegalArgumentException("파일 확장자가 존재하지 않습니다.");
+ }
+
+ public static ContentType findByExtension(final String extension) {
+ return Arrays.stream(ContentType.values())
+ .filter(contentType -> contentType.extension.equals(extension))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 파일형식입니다."));
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+
+ public String getHeaderValue() {
+ return headerValue;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java
new file mode 100644
index 0000000000..8da09bf0de
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11.message;
+
+public enum HttpStatus {
+ OK("OK", 200),
+ FOUND("Found", 302),
+ UNAUTHORIZED("Unauthorized", 401);
+
+ private final String message;
+ private final int code;
+
+ HttpStatus(final String message, final int code) {
+ this.message = message;
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java
new file mode 100644
index 0000000000..d102690605
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java
@@ -0,0 +1,31 @@
+package org.apache.coyote.http11.message.request;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Cookie {
+ private final Map cookieData;
+
+ private Cookie(final Map cookieData) {
+ this.cookieData = cookieData;
+ }
+
+ public static Cookie from(final String value) {
+ if (value == null) {
+ return new Cookie(Collections.emptyMap());
+ }
+
+ final String[] cookies = value.split("; ");
+ final Map cookieData = Arrays.stream(cookies)
+ .map(cookie -> cookie.split("="))
+ .collect(Collectors.toMap(cookiePair -> cookiePair[0], cookiePair -> cookiePair[1]));
+
+ return new Cookie(cookieData);
+ }
+
+ public String get(final String key) {
+ return cookieData.get(key);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java
new file mode 100644
index 0000000000..c25d03e574
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java
@@ -0,0 +1,27 @@
+package org.apache.coyote.http11.message.request;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class QueryParameter {
+ public static final QueryParameter EMPTY = new QueryParameter(new HashMap<>());
+ private Map parameters = new HashMap<>();
+
+ public QueryParameter(final Map parameters) {
+ this.parameters = parameters;
+ }
+
+ public QueryParameter(final String uri) {
+ final int index = uri.indexOf("?");
+ final String queryString = uri.substring(index + 1);
+ final String[] splitedQueryStrings = queryString.split("&");
+ for (final String str : splitedQueryStrings) {
+ final int strIndex = str.indexOf("=");
+ parameters.put(str.substring(0, strIndex), str.substring(strIndex + 1));
+ }
+ }
+
+ public Map getParameters() {
+ return parameters;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java
new file mode 100644
index 0000000000..ab43ab12c8
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java
@@ -0,0 +1,65 @@
+package org.apache.coyote.http11.message.request;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.Optional;
+import org.apache.coyote.http11.Session;
+import org.apache.coyote.http11.SessionManager;
+
+public class Request {
+ private final RequestLine requestLine;
+ private final RequestHeaders requestHeaders;
+ private final RequestForms requestForms;
+
+ public Request(final RequestLine requestLine, final RequestHeaders requestHeaders,
+ final RequestForms requestForms) {
+ this.requestLine = requestLine;
+ this.requestHeaders = requestHeaders;
+ this.requestForms = requestForms;
+ }
+
+ public static Request from(final BufferedReader br) throws IOException {
+ final RequestLine requestLine = RequestLine.from(br.readLine());
+ final RequestHeaders requestHeaders = RequestHeaders.from(br);
+ final RequestForms requestForms = createRequestBody(br, requestHeaders);
+ return new Request(requestLine, requestHeaders, requestForms);
+ }
+
+ private static RequestForms createRequestBody(final BufferedReader br, final RequestHeaders requestHeaders)
+ throws IOException {
+ if (!requestHeaders.hasContentType()) {
+ return new RequestForms(null);
+ }
+ final int contentLength = Integer.parseInt((String) requestHeaders.get("Content-Length"));
+ final char[] buffer = new char[contentLength];
+ br.read(buffer, 0, contentLength);
+ final String requestBody = new String(buffer);
+ return RequestForms.from(requestBody);
+ }
+
+ public boolean noSession() {
+ final String sessionId = requestHeaders.getCookieValue("JSESSIONID");
+ return SessionManager.findSession(sessionId) == null;
+ }
+
+ public Optional