* File 객체를 생성하려면 파일의 경로를 알아야 한다.
* 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다.
* resource 디렉터리의 경로는 어떻게 알아낼 수 있을까?
@@ -27,27 +29,25 @@ class FileTest {
void resource_디렉터리에_있는_파일의_경로를_찾는다() {
final String fileName = "nextstep.txt";
- // todo
- final String actual = "";
+ final ClassLoader classLoader = getClass().getClassLoader();
+ final URL url = classLoader.getResource(fileName);
+ final String actual = url.getFile();
assertThat(actual).endsWith(fileName);
}
/**
* 파일 내용 읽기
- *
+ *
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
- void 파일의_내용을_읽는다() {
+ void 파일의_내용을_읽는다() throws IOException {
final String fileName = "nextstep.txt";
+ final String fileUrl = getClass().getClassLoader().getResource(fileName).getFile();
- // todo
- final Path path = null;
-
- // todo
- final List actual = Collections.emptyList();
+ final List actual = Files.readAllLines(Path.of(fileUrl));
assertThat(actual).containsOnly("nextstep");
}
diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java
index 47a79356b6..a908f66871 100644
--- a/study/src/test/java/study/IOStreamTest.java
+++ b/study/src/test/java/study/IOStreamTest.java
@@ -4,19 +4,30 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import java.io.*;
+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 static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
- *
+ *
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
- *
+ *
* Stream은 데이터를 바이트로 읽고 쓴다.
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
@@ -26,7 +37,7 @@ class IOStreamTest {
/**
* OutputStream 학습하기
- *
+ *
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
* public abstract void write(int b) throws IOException;
@@ -39,7 +50,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바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
@@ -48,12 +59,8 @@ class OutputStream_학습_테스트 {
void OutputStream은_데이터를_바이트로_처리한다() throws IOException {
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();
assertThat(actual).isEqualTo("nextstep");
@@ -63,7 +70,7 @@ class OutputStream_학습_테스트 {
/**
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
- *
+ *
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
@@ -73,11 +80,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 +94,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 +104,12 @@ class OutputStream_학습_테스트 {
/**
* InputStream 학습하기
- *
+ *
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
* InputStream의 read() 메서드는 기반 메서드이다.
* public abstract int read() throws IOException;
- *
+ *
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
*/
@Nested
@@ -124,11 +125,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 +140,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 +150,7 @@ class InputStream_학습_테스트 {
/**
* FilterStream 학습하기
- *
+ *
* 필터는 필터 스트림, reader, writer로 나뉜다.
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
@@ -169,12 +164,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,16 +192,23 @@ 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 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
final StringBuilder actual = new StringBuilder();
+ String line = bufferedReader.readLine();
+ while (line != null) {
+ actual.append(line + "\r\n");
+ line = bufferedReader.readLine();
+ }
+
assertThat(actual).hasToString(emoji);
}
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
index 7f1b2c7e96..d50f800ebf 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,43 @@
package org.apache.coyote.http11;
+import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.exception.UncheckedServletException;
+import nextstep.jwp.model.User;
import org.apache.coyote.Processor;
+import org.apache.coyote.http11.cookie.HttpCookie;
+import org.apache.coyote.http11.request.HttpMethod;
+import org.apache.coyote.http11.request.HttpRequest;
+import org.apache.coyote.http11.request.HttpRequestStartLine;
+import org.apache.coyote.http11.response.HttpResponseEntity;
+import org.apache.coyote.http11.session.HttpSession;
+import org.apache.coyote.http11.session.SessionManager;
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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
public class Http11Processor implements Runnable, Processor {
-
private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
+ private static final SessionManager sessionManager = SessionManager.create();
+ private static final String STATIC = "static";
+ private static final String INDEX_HTML = "/index.html";
+ private static final String UNAUTHORIZED_HTML = "/401.html";
+ private static final String LOGIN_HTML = "/login.html";
+ private static final String BODY_DELIMITER = "&";
+ private static final String PAIR_DELIMITER = "=";
+ private static final int KEY_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
private final Socket connection;
@@ -26,22 +53,93 @@ public void run() {
@Override
public void process(final Socket connection) {
- try (final var inputStream = connection.getInputStream();
- final var outputStream = connection.getOutputStream()) {
+ try (final var bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+ final var bufferedWriter = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) {
- final var responseBody = "Hello world!";
+ final HttpRequest httpRequest = HttpRequest.from(bufferedReader);
+ final HttpCookie httpCookie = makeHttpCookie(httpRequest);
- 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);
+ final HttpResponseEntity httpResponseEntity = makeResponseEntity(httpRequest, httpCookie);
+ final String response = httpResponseEntity.makeResponse();
- outputStream.write(response.getBytes());
- outputStream.flush();
+ bufferedWriter.write(response);
+ bufferedWriter.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
}
+
+ private HttpCookie makeHttpCookie(final HttpRequest httpRequest) {
+ if (httpRequest.hasCookie()) {
+ return HttpCookie.from(httpRequest.getCookie());
+ }
+ return HttpCookie.empty();
+ }
+
+ private HttpResponseEntity makeResponseEntity(final HttpRequest httpRequest, final HttpCookie cookie) throws IOException {
+ final HttpRequestStartLine startLine = httpRequest.getStartLine();
+ final String path = startLine.getPath();
+
+ if (startLine.getHttpMethod().equals(HttpMethod.POST)) {
+ if (path.startsWith("/login")) {
+ return login(httpRequest);
+ }
+ if (path.startsWith("/register")) {
+ return register(httpRequest);
+ }
+ }
+ if (path.startsWith("/login") && cookie.hasJSESSIONID()) {
+ final String jsessionid = cookie.getJSESSIONID();
+ HttpSession httpSession = sessionManager.findSession(jsessionid);
+ if (Objects.isNull(httpSession)) {
+ return HttpResponseEntity.ok(LOGIN_HTML, makeResponseBody(LOGIN_HTML));
+ }
+ return HttpResponseEntity.found(INDEX_HTML);
+ }
+ return HttpResponseEntity.ok(path, makeResponseBody(path));
+ }
+
+ private HttpResponseEntity login(final HttpRequest httpRequest) throws IOException {
+ final Map loginData = parseFormData(httpRequest.getBody());
+ final User user = InMemoryUserRepository.findByAccount(loginData.get("account"))
+ .orElseThrow();
+ if (user.checkPassword(loginData.get("password"))) {
+ final HttpCookie newCookie = HttpCookie.create();
+ saveSession(newCookie, user);
+ return HttpResponseEntity.found(INDEX_HTML)
+ .setCookie(newCookie.getJSESSIONID());
+ }
+ return HttpResponseEntity.ok(UNAUTHORIZED_HTML, makeResponseBody(UNAUTHORIZED_HTML));
+ }
+
+ private void saveSession(final HttpCookie newCookie, final User user) {
+ final HttpSession httpSession = new HttpSession(newCookie.getJSESSIONID());
+ httpSession.setAttribute("user", user);
+ sessionManager.add(httpSession);
+ }
+
+ private HttpResponseEntity register(final HttpRequest httpRequest) throws IOException {
+ final Map registerData = parseFormData(httpRequest.getBody());
+ InMemoryUserRepository.save(new User(registerData.get("account"), registerData.get("password"), registerData.get("email")));
+ return HttpResponseEntity.ok(INDEX_HTML, makeResponseBody(INDEX_HTML));
+ }
+
+ private Map parseFormData(final String body) {
+ return Arrays.stream(body.split(BODY_DELIMITER))
+ .map(data -> data.split(PAIR_DELIMITER))
+ .collect(Collectors.toMap(
+ data -> data[KEY_INDEX],
+ data -> data[VALUE_INDEX])
+ );
+ }
+
+ private String makeResponseBody(final String path) throws IOException {
+ if (path.equals("/")) {
+ return "Hello world!";
+ }
+ final ClassLoader classLoader = getClass().getClassLoader();
+ final String filePath = classLoader.getResource(STATIC + path).getPath();
+ final String fileContent = new String(Files.readAllBytes(Path.of(filePath)));
+ return String.join("\r\n", fileContent);
+ }
}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java
new file mode 100644
index 0000000000..d286c2ea6a
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java
@@ -0,0 +1,47 @@
+package org.apache.coyote.http11.cookie;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public class HttpCookie {
+ private static final String JSESSIONID = "JSESSIONID";
+ private static final String LIST_DELIMITER = "; ";
+ private static final String PAIR_DELIMITER = "=";
+ private static final int KEY_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
+
+ private final Map cookie;
+
+ private HttpCookie(final Map cookie) {
+ this.cookie = cookie;
+ }
+
+ public static HttpCookie from(final String cookie) {
+ return new HttpCookie(Arrays.stream(cookie.split(LIST_DELIMITER))
+ .map(data -> data.split(PAIR_DELIMITER))
+ .collect(Collectors.toMap(
+ data -> data[KEY_INDEX],
+ data -> data[VALUE_INDEX])
+ )
+ );
+ }
+
+ public static HttpCookie empty() {
+ return new HttpCookie(Collections.emptyMap());
+ }
+
+ public static HttpCookie create() {
+ return new HttpCookie(Map.of(JSESSIONID, String.valueOf(UUID.randomUUID())));
+ }
+
+ public boolean hasJSESSIONID() {
+ return this.cookie.containsKey(JSESSIONID);
+ }
+
+ public String getJSESSIONID() {
+ return this.cookie.get(JSESSIONID);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java
new file mode 100644
index 0000000000..024086f1f4
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java
@@ -0,0 +1,6 @@
+package org.apache.coyote.http11.request;
+
+public enum HttpMethod {
+ GET,
+ POST
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java
new file mode 100644
index 0000000000..83c51359ca
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java
@@ -0,0 +1,63 @@
+package org.apache.coyote.http11.request;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class HttpRequest {
+ private final HttpRequestStartLine startLine;
+ private final HttpRequestHeaders headers;
+ private final HttpRequestBody body;
+
+ private HttpRequest(final HttpRequestStartLine startLine, final HttpRequestHeaders headers, final HttpRequestBody body) {
+ this.startLine = startLine;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ public static HttpRequest from(final BufferedReader bufferedReader) throws IOException {
+ final List requestHeader = extractRequestHeader(bufferedReader);
+ final HttpRequestStartLine startLine = HttpRequestStartLine.from(requestHeader.get(0));
+ final HttpRequestHeaders headers = HttpRequestHeaders.from(requestHeader.subList(1, requestHeader.size()));
+ final HttpRequestBody requestBody = extractRequestBody(bufferedReader, headers);
+ return new HttpRequest(startLine, headers, requestBody);
+ }
+
+ private static List extractRequestHeader(final BufferedReader bufferedReader) throws IOException {
+ final List requestHeader = new ArrayList<>();
+ String line = bufferedReader.readLine();
+ while (!"".equals(line) && line != null) {
+ requestHeader.add(line);
+ line = bufferedReader.readLine();
+ }
+ return requestHeader;
+ }
+
+ private static HttpRequestBody extractRequestBody(final BufferedReader bufferedReader, final HttpRequestHeaders requestHeader) throws IOException {
+ final String contentLength = requestHeader.get("Content-Length");
+ if (contentLength == null) {
+ return HttpRequestBody.empty();
+ }
+ final int length = Integer.parseInt(contentLength);
+ final char[] buffer = new char[length];
+ bufferedReader.read(buffer, 0, length);
+ return HttpRequestBody.from(new String(buffer));
+ }
+
+ public boolean hasCookie() {
+ return headers.hasCookie();
+ }
+
+ public HttpRequestStartLine getStartLine() {
+ return startLine;
+ }
+
+ public String getBody() {
+ return body.getBody();
+ }
+
+ public String getCookie() {
+ return headers.get("Cookie");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java
new file mode 100644
index 0000000000..f1e84ee1f9
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11.request;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class HttpRequestBody {
+ private final String body;
+
+ private HttpRequestBody(final String body) {
+ this.body = body;
+ }
+
+ public static HttpRequestBody from(final String httpRequestBody) {
+ return new HttpRequestBody(httpRequestBody);
+ }
+
+ public static HttpRequestBody empty() {
+ return new HttpRequestBody(StringUtils.EMPTY);
+ }
+
+ public String getBody() {
+ return body;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java
new file mode 100644
index 0000000000..6ade8d60ab
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java
@@ -0,0 +1,35 @@
+package org.apache.coyote.http11.request;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class HttpRequestHeaders {
+ private static final String PAIR_DELIMITER = ": ";
+ private static final int KEY_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
+
+ private final Map headers;
+
+ private HttpRequestHeaders(final Map headers) {
+ this.headers = headers;
+ }
+
+ public static HttpRequestHeaders from(final List httpRequestHeader) {
+ return new HttpRequestHeaders(httpRequestHeader.stream()
+ .map(data -> data.split(PAIR_DELIMITER))
+ .collect(Collectors.toMap(
+ data -> data[KEY_INDEX],
+ data -> data[VALUE_INDEX])
+ )
+ );
+ }
+
+ public boolean hasCookie() {
+ return headers.containsKey("Cookie");
+ }
+
+ public String get(final String key) {
+ return headers.get(key);
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java
new file mode 100644
index 0000000000..7550c3cecc
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java
@@ -0,0 +1,49 @@
+package org.apache.coyote.http11.request;
+
+public class HttpRequestStartLine {
+ private static final String QUERY_DELIMITER = "?";
+ private static final int METHOD_INDEX = 0;
+ private static final int PATH_INDEX = 1;
+ private static final int VERSION_INDEX = 2;
+
+ private final HttpMethod httpMethod;
+ private final String path;
+ private final Query query;
+ private final String version;
+
+ private HttpRequestStartLine(final HttpMethod httpMethod, final String path, final Query query, final String version) {
+ this.httpMethod = httpMethod;
+ this.path = path;
+ this.query = query;
+ this.version = version;
+ }
+
+ public static HttpRequestStartLine from(final String startLine) {
+ final String[] startLines = startLine.split(" ");
+ String path = startLines[PATH_INDEX];
+ final Query query = extractQuery(path);
+ if (path.equals("/login") || path.equals("/register")) {
+ path = path + ".html";
+ }
+ return new HttpRequestStartLine(HttpMethod.valueOf(startLines[METHOD_INDEX]), path, query, startLines[VERSION_INDEX]);
+ }
+
+ private static Query extractQuery(final String path) {
+ if (isExistQuery(path)) {
+ return Query.from(path.substring(path.indexOf(QUERY_DELIMITER) + PATH_INDEX));
+ }
+ return Query.empty();
+ }
+
+ private static boolean isExistQuery(final String path) {
+ return path.contains(QUERY_DELIMITER);
+ }
+
+ public HttpMethod getHttpMethod() {
+ return httpMethod;
+ }
+
+ public String getPath() {
+ return path;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java
new file mode 100644
index 0000000000..9e7cd03a1d
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java
@@ -0,0 +1,32 @@
+package org.apache.coyote.http11.request;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class Query {
+ private static final String QUERY_DELIMITER = "&";
+ private static final String PAIR_DELIMITER = "=";
+ private static final int KEY_INDEX = 0;
+ private static final int VALUE_INDEX = 1;
+
+ private final Map query;
+
+ private Query(final Map query) {
+ this.query = query;
+ }
+
+ public static Query from(final String query) {
+ return new Query(Arrays.stream(query.split(QUERY_DELIMITER))
+ .map(data -> data.split(PAIR_DELIMITER))
+ .collect(Collectors.toMap(
+ data -> data[KEY_INDEX],
+ data -> data[VALUE_INDEX]
+ )));
+ }
+
+ public static Query empty() {
+ return new Query(Collections.emptyMap());
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java
new file mode 100644
index 0000000000..6116aa8635
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java
@@ -0,0 +1,54 @@
+package org.apache.coyote.http11.response;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.StringJoiner;
+
+public class HttpResponseEntity {
+ private final Map header;
+ private final String body;
+
+ private HttpResponseEntity(final Map header, final String body) {
+ this.header = header;
+ this.body = body;
+ }
+
+ public static HttpResponseEntity ok(final String path, final String body) {
+ final Map requestHeader = new LinkedHashMap<>();
+ requestHeader.put("HTTP/1.1 ", HttpStatusCode.OK.message());
+ requestHeader.put("Content-Type: ", makeContentType(path) + ";charset=utf-8 ");
+ requestHeader.put("Content-Length: ", body.getBytes().length + " ");
+ return new HttpResponseEntity(requestHeader, body);
+ }
+
+ private static String makeContentType(final String path) {
+ if (path.endsWith("css")) {
+ return "text/css";
+ }
+ return "text/html";
+ }
+
+ public static HttpResponseEntity found(final String path) {
+ final Map requestHeader = new LinkedHashMap<>();
+ requestHeader.put("HTTP/1.1 ", HttpStatusCode.FOUND.message());
+ requestHeader.put("Location: ", path);
+ return new HttpResponseEntity(requestHeader, StringUtils.EMPTY);
+ }
+
+ public HttpResponseEntity setCookie(final String jsessionid) {
+ header.put("Set-Cookie: JSESSIONID=", jsessionid);
+ return this;
+ }
+
+ public String makeResponse() {
+ StringJoiner stringJoiner = new StringJoiner("\r\n");
+ for (final Map.Entry entry : header.entrySet()) {
+ stringJoiner.add(entry.getKey() + entry.getValue());
+ }
+ stringJoiner.add("");
+ stringJoiner.add(body);
+ return stringJoiner.toString();
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java
new file mode 100644
index 0000000000..483d4db414
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java
@@ -0,0 +1,19 @@
+package org.apache.coyote.http11.response;
+
+public enum HttpStatusCode {
+ OK(200, "OK"),
+ FOUND(302, "Found"),
+ ;
+
+ private final int code;
+ private final String text;
+
+ HttpStatusCode(int code, String text) {
+ this.code = code;
+ this.text = text;
+ }
+
+ public String message() {
+ return String.join(" ", String.valueOf(code), text, "");
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java b/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java
new file mode 100644
index 0000000000..f2c7147484
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java
@@ -0,0 +1,30 @@
+package org.apache.coyote.http11.session;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpSession {
+
+ private final String id;
+ private final Map items = new HashMap<>();
+
+ public HttpSession(final String id) {
+ this.id = id;
+ }
+
+ public Object getAttribute(final String name) {
+ return items.get(name);
+ }
+
+ public void setAttribute(final String name, final Object value) {
+ items.put(name, value);
+ }
+
+ public void removeAttribute(final String name) {
+ items.remove(name);
+ }
+
+ public String getId() {
+ return id;
+ }
+}
diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java
new file mode 100644
index 0000000000..49a42e7dc6
--- /dev/null
+++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java
@@ -0,0 +1,23 @@
+package org.apache.coyote.http11.session;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SessionManager {
+ private static final Map SESSIONS = new HashMap<>();
+
+ private SessionManager() {
+ }
+
+ public static SessionManager create() {
+ return new SessionManager();
+ }
+
+ public void add(final HttpSession httpSession) {
+ SESSIONS.put(httpSession.getId(), httpSession);
+ }
+
+ public HttpSession findSession(final String id) {
+ return SESSIONS.get(String.valueOf(id));
+ }
+}
diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html
index f4ed9de875..ff5bdacd5d 100644
--- a/tomcat/src/main/resources/static/login.html
+++ b/tomcat/src/main/resources/static/login.html
@@ -1,66 +1,70 @@
-
-
-
-
-
-
- 로그인
-
-
-
-
-
-
-
-
-
-
-
-
로그인
-
-
+
+
+
+
+
+
+ 로그인
+
+
+
+
+
+
+
+
+
+
+
+
로그인
+
+
+
+
-
+
-
+
+
+
+
diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java
index 512b919f09..f25837ebbb 100644
--- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java
+++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java
@@ -1,8 +1,8 @@
package nextstep.org.apache.coyote.http11;
-import support.StubSocket;
import org.apache.coyote.http11.Http11Processor;
import org.junit.jupiter.api.Test;
+import support.StubSocket;
import java.io.File;
import java.io.IOException;
@@ -36,7 +36,7 @@ void process() {
@Test
void index() throws IOException {
// given
- final String httpRequest= String.join("\r\n",
+ final String httpRequest = String.join("\r\n",
"GET /index.html HTTP/1.1 ",
"Host: localhost:8080 ",
"Connection: keep-alive ",
@@ -54,7 +54,149 @@ void index() throws IOException {
var expected = "HTTP/1.1 200 OK \r\n" +
"Content-Type: text/html;charset=utf-8 \r\n" +
"Content-Length: 5564 \r\n" +
- "\r\n"+
+ "\r\n" +
+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
+
+ assertThat(socket.output()).isEqualTo(expected);
+ }
+
+ @Test
+ void css() throws IOException {
+ // given
+ final String httpRequest = String.join("\r\n",
+ "GET /css/styles.css HTTP/1.1 ",
+ "Host: localhost:8080 ",
+ "Connection: keep-alive ",
+ "Accept: text/css,*/*;q=0.1",
+ "",
+ "");
+
+ final var socket = new StubSocket(httpRequest);
+ final Http11Processor processor = new Http11Processor(socket);
+
+ // when
+ processor.process(socket);
+
+ // then
+ final URL resource = getClass().getClassLoader().getResource("static/css/styles.css");
+ var expected = "HTTP/1.1 200 OK \r\n" +
+ "Content-Type: text/css;charset=utf-8 \r\n" +
+ "Content-Length: 211991 \r\n" +
+ "\r\n" +
+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
+
+ assertThat(socket.output()).isEqualTo(expected);
+ }
+
+ @Test
+ void login() throws IOException {
+ // given
+ final String httpRequest = String.join("\r\n",
+ "GET /login HTTP/1.1 ",
+ "Host: localhost:8080 ",
+ "Connection: keep-alive ",
+ "",
+ "");
+
+ final var socket = new StubSocket(httpRequest);
+ final Http11Processor processor = new Http11Processor(socket);
+
+ // when
+ processor.process(socket);
+
+ // then
+ final URL resource = getClass().getClassLoader().getResource("static/login.html");
+ var expected = "HTTP/1.1 200 OK \r\n" +
+ "Content-Type: text/html;charset=utf-8 \r\n" +
+ "Content-Length: 3447 \r\n" +
+ "\r\n" +
+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
+
+ assertThat(socket.output()).isEqualTo(expected);
+ }
+
+ @Test
+ void loginFail() throws IOException {
+ // given
+ final String httpRequest = String.join("\r\n",
+ "POST /login HTTP/1.1 ",
+ "Host: localhost:8080 ",
+ "Connection: keep-alive ",
+ "Content-Length: 26",
+ "Content-Type: application/x-www-form-urlencoded",
+ "Accept: */*",
+ "",
+ "account=gugu&password=1234");
+
+ final var socket = new StubSocket(httpRequest);
+ final Http11Processor processor = new Http11Processor(socket);
+
+ // when
+ processor.process(socket);
+
+ // then
+ final URL resource = getClass().getClassLoader().getResource("static/401.html");
+ var expected = "HTTP/1.1 200 OK \r\n" +
+ "Content-Type: text/html;charset=utf-8 \r\n" +
+ "Content-Length: 2426 \r\n" +
+ "\r\n" +
+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
+
+ assertThat(socket.output()).isEqualTo(expected);
+ }
+
+ @Test
+ void register() throws IOException {
+ // given
+ final String httpRequest = String.join("\r\n",
+ "GET /register HTTP/1.1 ",
+ "Host: localhost:8080 ",
+ "Connection: keep-alive ",
+ "",
+ "");
+
+ final var socket = new StubSocket(httpRequest);
+ final Http11Processor processor = new Http11Processor(socket);
+
+ // when
+ processor.process(socket);
+
+ // then
+ final URL resource = getClass().getClassLoader().getResource("static/register.html");
+ var expected = "HTTP/1.1 200 OK \r\n" +
+ "Content-Type: text/html;charset=utf-8 \r\n" +
+ "Content-Length: 4319 \r\n" +
+ "\r\n" +
+ new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
+
+ assertThat(socket.output()).isEqualTo(expected);
+ }
+
+ @Test
+ void registerSuccess() throws IOException {
+ // given
+ final String httpRequest = String.join("\r\n",
+ "POST /register HTTP/1.1 ",
+ "Host: localhost:8080 ",
+ "Connection: keep-alive ",
+ "Content-Length: 80",
+ "Content-Type: application/x-www-form-urlencoded",
+ "Accept: */*",
+ "",
+ "account=gugu&password=password&email=hkkang%40woowahan.com");
+
+ final var socket = new StubSocket(httpRequest);
+ final Http11Processor processor = new Http11Processor(socket);
+
+ // when
+ processor.process(socket);
+
+ // then
+ final URL resource = getClass().getClassLoader().getResource("static/index.html");
+ var expected = "HTTP/1.1 200 OK \r\n" +
+ "Content-Type: text/html;charset=utf-8 \r\n" +
+ "Content-Length: 5564 \r\n" +
+ "\r\n" +
new String(Files.readAllBytes(new File(resource.getFile()).toPath()));
assertThat(socket.output()).isEqualTo(expected);