diff --git a/README.md b/README.md
index e027a83a3..7ce1b5348 100644
--- a/README.md
+++ b/README.md
@@ -17,138 +17,27 @@ refactor
test (when adding missing tests)
chore (maintain)
```
-## 기능 정의
-### Step1. HTTP Header를 파싱한다.
- ```text/plain
- GET /docs/index.html HTTP/1.1
- Host: www.nowhere123.com
- Accept: image/gif, image/jpeg, */*
- Accept-Language: en-us
- Accept-Encoding: gzip, deflate
- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
- (blank line)
- ```
-
-
-HttpHeaderParser
-- response header 문자열을 파싱 한다.
-- request header 문자열을 파싱 한다.
-
-
-- HttpRequestParser
- - HttpRequestLineParser
- - request line을 파싱 한다.
- - uri는 쿼리 파라미터도 같이 파싱한다.
- - HttpRequestLine 객체를 생성한다.
-
-
-- HttpResponseParser
- - HttpStatusLineParser
- - status line을 파싱 한다.
- - HttpStatusLine 객체를 생성 한다.
-
-- HttpResponse를 생성 한다.
- - HttpHeader, (HttpContents) 객체를 포함한다.
- - 기본 Response가 있으며 사용자에 의해 교체 가능 해야 한다.
-
-- HttpRequest를 생성 한다.
- - HttpHeader, (HttpContents) 객체를 포함한다.
- - 기본 Request가 있으며 사용자에 의해 교체 가능 해야 한다.
- - 필수 입력 파라미터가 존재한다.(ex. uri, method)
-
-
-
-
-
-
-
-
-
-
-- HTTP 요청을 받거나 응답을 보낸다.
-- 요청 메시지는 request line, headers, contents로 나누어 진다.
-- 응답 메시지는 status line, headers, contents로 나누어 진다.
-- header는 여러 개의 키와 값을 포함한다.
-- header는 키와 값을 구분하는 구분자는 :로 한다. : 사이에는 공백이 존재할 수도 있다.
-- version은 protocol name, major version, minor version로 구분된다.
-- status line은 version, status code, reason phrase로 구분된다.
-- 프레임워크 제공의 입장에서 보았을 때 GET과 POST구분은 사용자의 필요에 의해서 생성되어야 할 수도 있다.
- - 즉 프레임워크 입장에서 method는 입력 되는 데이터 중 하나이다.
- - 단지 method에 따라 처리 해주어야 하는 방법이 달라지는 것이다.
-- contents를 포함하는 경우와 포함하지 않는 경우가 존재한다.
-- 다수의 요청을 처리할 때 모든 컨텐츠를 힙에 올리지 않아야 한다.
- - 올리길 원하는 경우 설정 값으로 컨트롤 할 수 있게 만들어야 한다.
-- request 라인은 SP로 구분된다.
-- 헤더와 컨텐츠 사이에는 CRLF로 구분된다.
-- uri는 쿼리 파라미터도 파싱되어야 한다.
- - uri 형태는 전략에 따라 다른 형태를 가진다. 즉 전략에 따라 서버에서 처리할 수 있는 방법이 달라진다.
-- 헤더의 key들은 직접 입력 할 경우 오기입의 여지가 있으므로 미리 정의하여 제공한다.
-
-
-
-- HttpRequest
- - RequestLine
- - method: GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
- - uri: /docs/index.html
- - version: HTTP/1.1
- - protocolName
- - majorVersion
- - minorVersion
- - headers:
- - key: Accept
- - value: image/gif, image/jpeg, */*
- - contents:
- - (blank line)
- - chunked: false
-
-- HttpResponse
- - StatusLine
- - version: HTTP/1.1
- - status: 200, 404, 500, ...
- - reason: OK, Not Found, Internal Server Error, ...
- - headers:
- - Content-Type: text/html
- - Content-Length: 1234
- - contents:
- - (blank line)
-
-- HttpContents
- - contents:
- - \
- - \
- - \Hello, World!\<\/title\>
- - \<\/head\>
- - \
- - \Hello, World!\<\/h1\>
- - \<\/body\>
- - \<\/html\>
-
-- HttpChunk
- - chunk:
- - chunk-size: 1234
- - chunk-data: ...
- - (blank line)
- - last-chunk:
- - chunk-size: 0
- - (blank line)
-
-
- ---
- - [참고] RFC 7230/7231 스펙
- * 모든 HTTP/1.1 메시지는 시작 라인과 그 뒤에 오는 시퀀스로 구성됩니다.
- * 시작 라인은 아래와 같이 구성되며 각 요소 사이는 single space(SP)로 구분됩니다.
- ```text/plain
- request-line = method SP request-target SP HTTP-version CRLF
- ```
- * request-target은 적용 할 대상 리소스를 식별합니다 서버가 해석할 수 있는 것보다 길다면 414로 응답 합니다.
- * method 토큰은 대상 리소스에서 수행 할 요청 방법을 나타내며 대소문자를 구분합니다.
- * 유효하지 않은 요청 라인을 수신한 경우 400(잘못된 요청)으로 응답합니다.
- * RFC 7302는 content-length 없는 head를 허용한다.
- * GET 캐시
- * Cache-Control 헤더 필드에 의해 달리 표시되지 않는 한 캐시 사용 가능
- * POST 캐시
- * 기존과 동일한 처리를 요구하는 경우 303으로 응답하여 캐시된 해당 리소스로 리다이렉션 시킨다.
- * Location 필드에 해당 위치를 표시
- * user-agent에 아직 캐시되지 않은 경우 추가 요청 비용이 발생
-
+## 요구 사항
+-[x] request를 분석할 수 있다.
+ - Request Line을 파싱한다.
+ - Method, Path, Protocol, Version을 파싱한다.
+ - Path에서 Query를 분리하여 파싱한다.
+
+-[x] 사용자가 접속하면 /index.html view를 볼 수 있다.
+ - Headers를 파싱한다.
+ - path에 해당하는 위치를 찾는다
+ - 해당 위치에 /index.html을 읽어서 응답에 포함시킨다.
+ - 해당위치에 리소스가 존재하지 않는 경우 404를 반환한다.
+-[x] 회원가입을 요청하면 사용자는 회원가입을 할 수 있다.
+-[x] 회원가입을 완료하면 /index.html로 리다이렉션 되어야 한다.
+-[x] 사용자는 /user/login.html을 통하여 로그인이 가능하다.
+-[x] /user/list에서 사용자 목록을 볼 수 있다. 로그인 상태가 아니라면 login.html로 리다이렉션 된다.
+-[x] stylesheet 파일을 지원한다.
+
+-[x] 세션지원으로 로그인 상태를 유지할 수 있다.
+-[x] 클라이언트는 쿠키를 통해 세션아이디를 서버에 전달한다.
+-[x] 서버는 세션아이디를 통해 세션을 찾는다.
+-[x] 세션이 존재하지 않는 경우 새로운 세션을 생성한다.
+
+-[x] 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거한다.
diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java
index b7abb7304..50413e659 100644
--- a/src/main/java/model/User.java
+++ b/src/main/java/model/User.java
@@ -1,36 +1,40 @@
package model;
public class User {
- private String userId;
- private String password;
- private String name;
- private String email;
-
- public User(String userId, String password, String name, String email) {
- this.userId = userId;
- this.password = password;
- this.name = name;
- this.email = email;
- }
+ private String userId;
+ private String password;
+ private String name;
+ private String email;
- public String getUserId() {
- return userId;
- }
+ public User(String userId, String password, String name, String email) {
+ this.userId = userId;
+ this.password = password;
+ this.name = name;
+ this.email = email;
+ }
- public String getPassword() {
- return password;
- }
+ public String getUserId() {
+ return userId;
+ }
- public String getName() {
- return name;
- }
+ public String getPassword() {
+ return password;
+ }
- public String getEmail() {
- return email;
- }
+ public String getName() {
+ return name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
- @Override
- public String toString() {
- return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]";
+ public boolean matchPassword(String password) {
+ return this.password.equals(password);
}
+
+ @Override
+ public String toString() {
+ return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]";
+ }
}
diff --git a/src/main/java/webserver/HttpMessage.java b/src/main/java/webserver/HttpMessage.java
deleted file mode 100644
index 8a65cb467..000000000
--- a/src/main/java/webserver/HttpMessage.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package webserver;
-
-import java.util.HashMap;
-
-public class HttpMessage {
- private HashMap headers;
- private HttpVersion httpVersion;
-
- public HttpMessage(HashMap headers, HttpVersion httpVersion) {
- this.headers = headers;
- this.httpVersion = httpVersion;
- }
-
- public HashMap getHeaders() {
- return headers;
- }
-
- public HttpVersion getHttpVersion() {
- return httpVersion;
- }
-}
diff --git a/src/main/java/webserver/HttpMessageByteProcessor.java b/src/main/java/webserver/HttpMessageByteProcessor.java
deleted file mode 100644
index 878ffc8d8..000000000
--- a/src/main/java/webserver/HttpMessageByteProcessor.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package webserver;
-
-import io.netty.util.ByteProcessor;
-
-public interface HttpMessageByteProcessor extends ByteProcessor {
- byte EQUALS = (byte)'=';
- byte AMPERSAND = (byte)'&';
-
- byte QUESTION_MARK = (byte)'?';
-
- byte SLASH = (byte)'/';
-
- byte CR = (byte)'\r';
- byte LF = (byte)'\n';
- byte COLON = (byte)':';
- byte SPACE = (byte)' ';
- byte TAB = (byte)'\t';
-
- ByteProcessor FIND_EQUALS = new IndexOfProcessor(EQUALS);
- ByteProcessor FIND_AMPERSAND = new IndexOfProcessor(AMPERSAND);
-
- ByteProcessor FIND_QUESTION_MARK = new IndexOfProcessor(QUESTION_MARK);
-
- ByteProcessor IS_LINEAR_WHITESPACE = new ByteProcessor() {
- @Override
- public boolean process(byte value) {
- return value == SPACE || value == TAB;
- }
- };
-
- ByteProcessor IS_NOT_SLASH = new ByteProcessor() {
- @Override
- public boolean process(byte value) {
- return value != SLASH;
- }
- };
-
- ByteProcessor IS_CR = new ByteProcessor() {
- @Override
- public boolean process(byte value) {
- return value == CR;
- }
- };
-}
diff --git a/src/main/java/webserver/HttpMessageDecoder.java b/src/main/java/webserver/HttpMessageDecoder.java
deleted file mode 100644
index 3a3dfe490..000000000
--- a/src/main/java/webserver/HttpMessageDecoder.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package webserver;
-
-import java.util.HashMap;
-
-import io.netty.buffer.ByteBuf;
-
-public class HttpMessageDecoder {
- protected LineDecoder lineDecoder;
-
- public HttpMessageDecoder(LineDecoder lineDecoder) {
- this.lineDecoder = lineDecoder;
- }
-
- protected HashMap decodeHeaders() {
- return new HashMap<>();
- }
-
- protected ByteBuf readLine(ByteBuf buffer){
- int offset = buffer.forEachByte(HttpMessageByteProcessor.FIND_CRLF);
- return buffer.readRetainedSlice(offset);
- }
-
- protected ByteBuf[] readLines(ByteBuf buffer) {
- return null;
- }
-}
diff --git a/src/main/java/webserver/HttpRequest.java b/src/main/java/webserver/HttpRequest.java
deleted file mode 100644
index 64c7754e7..000000000
--- a/src/main/java/webserver/HttpRequest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package webserver;
-
-import java.util.HashMap;
-
-public class HttpRequest extends HttpMessage {
- private RequestLine requestLine;
-
- public HttpRequest(RequestLine requestLine, HashMap headers, HttpVersion httpVersion) {
- super(headers, httpVersion);
- this.requestLine = requestLine;
- }
-}
diff --git a/src/main/java/webserver/HttpRequestDecoder.java b/src/main/java/webserver/HttpRequestDecoder.java
deleted file mode 100644
index e1a770fcb..000000000
--- a/src/main/java/webserver/HttpRequestDecoder.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package webserver;
-
-import io.netty.buffer.ByteBuf;
-
-public class HttpRequestDecoder extends HttpMessageDecoder{
- public HttpRequestDecoder() {
- super(new RequestLineDecoder());
- }
-
- public HttpRequest decode(ByteBuf buffer) {
- RequestLine requestLine = lineDecoder.decode(readLine(buffer));
-
- return new HttpRequest(requestLine, decodeHeaders(), requestLine.getHttpVersion());
- }
-}
diff --git a/src/main/java/webserver/LineDecoder.java b/src/main/java/webserver/LineDecoder.java
deleted file mode 100644
index 5597421fa..000000000
--- a/src/main/java/webserver/LineDecoder.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package webserver;
-
-import io.netty.buffer.ByteBuf;
-
-public interface LineDecoder {
- int LINE_ELEMENT_SIZE = 3;
-
- RequestLine decode(ByteBuf buffer);
-}
diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java
index 8dd501115..a8d2731fe 100644
--- a/src/main/java/webserver/RequestHandler.java
+++ b/src/main/java/webserver/RequestHandler.java
@@ -1,6 +1,5 @@
package webserver;
-import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -9,62 +8,38 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufAllocator;
+import webserver.http.HttpRequest;
+import webserver.http.HttpRequestDecoder;
+import webserver.http.HttpResponse;
+import webserver.servlet.Servlet;
+import webserver.servlet.ServletMapping;
public class RequestHandler implements Runnable {
- private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
- private static final int MAX_BUFFER_SIZE = 8192;
+ private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);
+ private Socket connection;
+ private final HttpRequestDecoder httpRequestDecoder;
- private Socket connection;
- private HttpRequestDecoder httpRequestDecoder;
+ public RequestHandler(Socket connectionSocket) {
+ this.connection = connectionSocket;
+ this.httpRequestDecoder = new HttpRequestDecoder();
+ }
- public RequestHandler(Socket connectionSocket) {
- this.connection = connectionSocket;
- this.httpRequestDecoder = new HttpRequestDecoder();
- }
+ public void run() {
+ logger.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
+ connection.getPort());
- public void run() {
- logger.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
- connection.getPort());
+ try (InputStream inputStream = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
- try (InputStream inputStream = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
- // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다.
- // 사용자 요청을 읽어서 처리한 후 응답을 전송한다.
+ HttpRequest httpRequest = httpRequestDecoder.decode(inputStream);
+ HttpResponse httpResponse = new HttpResponse(out);
+ String servletPath = httpRequest.getServletPath();
- ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
- buffer.writeBytes(inputStream, MAX_BUFFER_SIZE);
+ Servlet servlet = ServletMapping.match(servletPath);
+ servlet.service(httpRequest, httpResponse);
- HttpRequest httpRequest = httpRequestDecoder.decode(buffer);
-
-
- DataOutputStream dos = new DataOutputStream(out);
- byte[] body = "Hello World".getBytes();
- response200Header(dos, body.length);
- responseBody(dos, body);
- } catch (IOException e) {
- logger.error(e.getMessage());
- }
- }
-
- private void response200Header(DataOutputStream dos, int lengthOfBodyContent) {
- try {
- dos.writeBytes("HTTP/1.1 200 OK \r\n");
- dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n");
- dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n");
- dos.writeBytes("\r\n");
- } catch (IOException e) {
- logger.error(e.getMessage());
- }
- }
-
- private void responseBody(DataOutputStream dos, byte[] body) {
- try {
- dos.write(body, 0, body.length);
- dos.flush();
- } catch (IOException e) {
- logger.error(e.getMessage());
- }
- }
+ } catch (IOException exception) {
+ logger.error(exception.getMessage());
+ }
+ }
}
diff --git a/src/main/java/webserver/RequestLine.java b/src/main/java/webserver/RequestLine.java
deleted file mode 100644
index 647308f45..000000000
--- a/src/main/java/webserver/RequestLine.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package webserver;
-
-public class RequestLine {
- private String method;
- private Uri uri;
- private HttpVersion httpVersion;
- public RequestLine(String requestMethod, Uri uri, HttpVersion httpVersion) {
- this.method = requestMethod;
- this.uri = uri;
- this.httpVersion = httpVersion;
- }
-
- public String getMethod() {
- return method;
- }
-
- public Uri getUri() {
- return uri;
- }
-
- public HttpVersion getHttpVersion() {
- return httpVersion;
- }
-}
diff --git a/src/main/java/webserver/RequestLineDecoder.java b/src/main/java/webserver/RequestLineDecoder.java
deleted file mode 100644
index 52f79b62f..000000000
--- a/src/main/java/webserver/RequestLineDecoder.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package webserver;
-
-import static webserver.HttpMessageByteProcessor.*;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import io.netty.buffer.ByteBuf;
-import io.netty.util.ByteProcessor;
-
-public class RequestLineDecoder implements LineDecoder{
- private static final int HTTP_METHOD_INDEX = 0;
- private static final int HTTP_URI_INDEX = 1;
- private static final int HTTP_VERSION_INDEX = 2;
-
- public RequestLine decode(ByteBuf buffer) {
- List buffers = splitLine(buffer);
- UriDecoder uriDecoder = new UriDecoder();
-
- String requestMethod = convertString(buffers.get(HTTP_METHOD_INDEX));
- Uri uri = uriDecoder.decode(buffers.get(HTTP_URI_INDEX));
- HttpVersion httpVersion = HttpVersion.valueOf(convertString(buffers.get(HTTP_VERSION_INDEX)));
- return new RequestLine(requestMethod, uri, httpVersion);
- }
-
- private List splitLine(ByteBuf buffer) {
- List requestLine = new ArrayList<>(LINE_ELEMENT_SIZE);
- while (buffer.isReadable()) {
- if (isIncluded(buffer, IS_LINEAR_WHITESPACE)) {
- throw new IllegalArgumentException("Invalid separator. only a single space or horizontal tab allowed.");
- }
-
- requestLine.add(sliceBuffer(buffer, FIND_LINEAR_WHITESPACE));
- }
-
- if (requestLine.size() < LINE_ELEMENT_SIZE) {
- throw new IllegalArgumentException(
- "Invalid argument count exception. There must be 3 elements, but received (" + requestLine.size()
- + ")");
- }
- return requestLine;
- }
-
- private boolean isIncluded(ByteBuf buffer, ByteProcessor processor) {
- try {
- return processor.process(buffer.getByte(buffer.readerIndex()));
- } catch (Exception exception) {
- throw new RuntimeException(exception);
- }
- }
-
- private void skipBytes(ByteBuf buffer) {
- int skipSize = 1;
- if(buffer.isReadable() && isIncluded(buffer, IS_CR)) {
- skipSize = 2;
- }
-
- if(!buffer.isReadable()){
- skipSize = 0;
- }
- buffer.skipBytes(skipSize);
- }
- private ByteBuf sliceBuffer(ByteBuf buffer, ByteProcessor byteProcessor) {
- int offset = findReadableLength(buffer, byteProcessor);
- ByteBuf subBuffer = buffer.readRetainedSlice(offset);
- skipBytes(buffer);
- return subBuffer;
- }
-
- private int findReadableLength(ByteBuf buffer, ByteProcessor byteProcessor) {
- int offset = buffer.forEachByte(
- buffer.readerIndex(), buffer.readableBytes(), byteProcessor);
-
- if (offset == -1) {
- int lastIndex = buffer.readerIndex() + buffer.readableBytes() - 1;
- offset = 0;
- if (buffer.getByte(lastIndex) == LF) {
- offset = -2;
- }
- offset += buffer.capacity();
- }
- return offset -= buffer.readerIndex();
- }
-
- private String convertString(ByteBuf buffer) {
- return buffer.toString(StandardCharsets.UTF_8);
- }
- private class UriDecoder {
- public Uri decode(ByteBuf buffer) {
- if (isIncluded(buffer, IS_NOT_SLASH)) {
- throw new IllegalArgumentException("Unsupported URI format. URI must start with '/'");
- }
-
- return new Uri(convertString(sliceBuffer(buffer, FIND_QUESTION_MARK)),
- decodeParams(buffer));
- }
-
- private HashMap decodeParams(ByteBuf buffer) {
- HashMap params = new HashMap<>();
-
- while (buffer.isReadable()) {
- ByteBuf keyValue = sliceBuffer(buffer, FIND_AMPERSAND);
-
- String key = convertString(sliceBuffer(keyValue, FIND_EQUALS));
- String value = convertString(sliceBuffer(keyValue, FIND_ASCII_SPACE));
-
- params.put(key, value);
- keyValue.release();
- }
- return params;
- }
- }
-}
diff --git a/src/main/java/webserver/Uri.java b/src/main/java/webserver/Uri.java
deleted file mode 100644
index d12022928..000000000
--- a/src/main/java/webserver/Uri.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package webserver;
-
-import java.util.Map;
-
-public class Uri {
- private String uri;
- private String path;
- private Map params;
-
- public Uri(String path, Map params) {
- this.path = path;
- this.params = params;
- }
-
- public String getUri() {
- return uri;
- }
-
- public String getPath() {
- return path;
- }
-
- public Map getParams() {
- return params;
- }
-}
diff --git a/src/main/java/webserver/http/Cookie.java b/src/main/java/webserver/http/Cookie.java
new file mode 100644
index 000000000..fee2dacaa
--- /dev/null
+++ b/src/main/java/webserver/http/Cookie.java
@@ -0,0 +1,81 @@
+package webserver.http;
+
+import java.util.Objects;
+
+public class Cookie {
+ private static final String DEFAULT_COOKIE_PATH = "/";
+ private String cookieName;
+ private String cookieValue;
+ private String cookiePath = DEFAULT_COOKIE_PATH;
+ private String cookieDomain;
+ private int cookieMaxAge;
+
+ public Cookie(String cookieName, String cookieValue) {
+ this.cookieName = cookieName;
+ this.cookieValue = cookieValue;
+ }
+
+ public void setCookieName(String cookieName) {
+ this.cookieName = cookieName;
+ }
+
+ public void setCookieValue(String cookieValue) {
+ this.cookieValue = cookieValue;
+ }
+
+ public void setCookiePath(String cookiePath) {
+ this.cookiePath = cookiePath;
+ }
+
+ public void setCookieDomain(String cookieDomain) {
+ this.cookieDomain = cookieDomain;
+ }
+
+ public void setCookieMaxAge(int cookieMaxAge) {
+ this.cookieMaxAge = cookieMaxAge;
+ }
+
+ public String getCookieName() {
+ return cookieName;
+ }
+
+ public String getCookieValue() {
+ return cookieValue;
+ }
+
+ public String toEncoded() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(cookieName)
+ .append("=")
+ .append(cookieValue);
+ if (cookiePath != null) {
+ builder.append("; Path=")
+ .append(cookiePath);
+ }
+ if (cookieDomain != null) {
+ builder.append("; Domain=")
+ .append(cookieDomain);
+ }
+ if (cookieMaxAge > 0) {
+ builder.append("; Max-Age=")
+ .append(cookieMaxAge);
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Cookie cookie = (Cookie)o;
+ return Objects.equals(cookieName, cookie.cookieName) && Objects.equals(cookieValue,
+ cookie.cookieValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cookieName, cookieValue);
+ }
+}
diff --git a/src/main/java/webserver/http/DefaultRequest.java b/src/main/java/webserver/http/DefaultRequest.java
new file mode 100644
index 000000000..5987c94e3
--- /dev/null
+++ b/src/main/java/webserver/http/DefaultRequest.java
@@ -0,0 +1,184 @@
+package webserver.http;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import com.google.common.base.Splitter;
+
+public class DefaultRequest implements HttpRequest {
+ private static final String SESSION_ID = "JSESSIONID";
+ private static final String COOKIE = "Cookie";
+ private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
+ private HttpMethod method;
+ private URI uri;
+ private String requestUri;
+ private String servletPath;
+ private String queryString;
+ private MultiValueMap queryParams;
+ private MultiValueMap headers;
+ private Map attributes;
+ private Map cookies;
+ private HttpVersion httpVersion;
+ private ByteBuffer inputChannel;
+
+ public DefaultRequest(RequestLine requestLine, MultiValueMap headers, ByteBuffer inputChannel) {
+ this.method = requestLine.getMethod();
+ this.uri = requestLine.getUri();
+ this.headers = headers;
+ this.httpVersion = requestLine.getHttpVersion();
+ this.inputChannel = inputChannel;
+ }
+
+ public boolean isFormRequest() {
+ if(headers.containsKey(HttpMessage.CONTENT_TYPE)) {
+ return getHeaders().get(HttpMessage.CONTENT_TYPE).stream()
+ .anyMatch(contentType -> contentType.contains(FORM_CONTENT_TYPE));
+ }
+ return false;
+ }
+
+ @Override
+ public MultiValueMap getQueryParams() {
+ if (CollectionUtils.isEmpty(queryParams)) {
+ if (isFormRequest()) {
+ return QueryStringDecoder.parseQueryString(inputChannel);
+ }
+ return QueryStringDecoder.parseQueryString(uri.getQuery());
+ }
+ return this.queryParams;
+ }
+
+ @Override
+ public String getParameter(String name) {
+ if (CollectionUtils.isEmpty(queryParams)) {
+ this.queryParams = getQueryParams();
+ }
+ return queryParams.getFirst(name);
+ }
+
+ @Override
+ public void addHeader(String key, String value) {
+ if (headers == null) {
+ headers = new LinkedMultiValueMap<>();
+ }
+ headers.add(key, value);
+ }
+
+ @Override
+ public MultiValueMap getHeaders() {
+ return this.headers;
+ }
+
+ @Override
+ public String getHeader(String key) {
+ return headers.getFirst(key);
+ }
+
+ public String getServletPath() {
+ if (Objects.isNull(servletPath)) {
+ servletPath = getRequestUri();
+ if (servletPath.contains(".")) {
+ int index = servletPath.lastIndexOf("/");
+ servletPath = servletPath.substring(0, index);
+ }
+ }
+ return servletPath;
+ }
+
+ @Override
+ public String getQueryString() {
+ if (Objects.isNull(queryString)) {
+ this.queryString = uri.getQuery();
+ }
+ return queryString;
+ }
+
+ @Override
+ public String getRequestUri() {
+ if (Objects.isNull(requestUri)) {
+ requestUri = uri.getPath();
+ }
+ return requestUri;
+ }
+
+ @Override
+ public HttpVersion getHttpVersion() {
+ return httpVersion;
+ }
+
+ @Override
+ public HttpMethod getMethod() {
+ return method;
+ }
+
+ @Override
+ public void setHttpVersion(HttpVersion httpVersion) {
+ this.httpVersion = httpVersion;
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+ attributes.put(name, value);
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return attributes.get(name);
+ }
+
+ @Override
+ public HttpSession getSession() {
+ SessionManager sessionManager = SessionManager.getInstance();
+ String sessionId = getRequestedSessionId();
+
+ if (sessionManager.isSessionIdValid(sessionId)) {
+ return sessionManager.findSession(sessionId);
+ }
+ HttpSession session = sessionManager.createSession(sessionId);
+ session.setChanged(true);
+ return session;
+ }
+
+ public Map getCookies() {
+ if (!CollectionUtils.isEmpty(cookies)) {
+ return cookies;
+ }
+ String cookieHeader = getHeader(COOKIE);
+ if (cookieHeader != null) {
+ cookies = initializeCookie(cookieHeader);
+ }
+ return cookies;
+ }
+
+ @Override
+ public String getRequestedSessionId() {
+ cookies = getCookies();
+ if (!CollectionUtils.isEmpty(cookies) && cookies.containsKey(SESSION_ID)) {
+ return cookies.get(SESSION_ID).getCookieValue();
+ }
+ return UUID.randomUUID().toString();
+ }
+
+ private Map initializeCookie(String cookieHeader) {
+ return Splitter.on(";")
+ .omitEmptyStrings()
+ .trimResults()
+ .withKeyValueSeparator("=")
+ .split(cookieHeader)
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, entry -> new Cookie(entry.getKey(), entry.getValue())));
+ }
+}
diff --git a/src/main/java/webserver/http/HttpMessage.java b/src/main/java/webserver/http/HttpMessage.java
new file mode 100644
index 000000000..2f620c300
--- /dev/null
+++ b/src/main/java/webserver/http/HttpMessage.java
@@ -0,0 +1,20 @@
+package webserver.http;
+
+import org.springframework.util.MultiValueMap;
+
+public interface HttpMessage {
+ String CONTENT_TYPE = "Content-Type";
+ String CONTENT_LENGTH = "Content-Length";
+ String SET_COOKIE = "Set-Cookie";
+ String LOCATION = "Location";
+
+ void addHeader(String key, String value);
+
+ String getHeader(String key);
+
+ void setHttpVersion(HttpVersion httpVersion);
+
+ MultiValueMap getHeaders();
+
+ HttpVersion getHttpVersion();
+}
diff --git a/src/main/java/webserver/http/HttpMessageDecoder.java b/src/main/java/webserver/http/HttpMessageDecoder.java
new file mode 100644
index 000000000..c67613cf3
--- /dev/null
+++ b/src/main/java/webserver/http/HttpMessageDecoder.java
@@ -0,0 +1,136 @@
+package webserver.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import com.google.common.collect.Lists;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.internal.AppendableCharSequence;
+
+public abstract class HttpMessageDecoder {
+ private static final int MAX_BUFFER_SIZE = 8192;
+ protected LineSplitter lineSplitter = new LineSplitter(new AppendableCharSequence(1024));
+
+ public final T decode(InputStream inputStream) throws IOException {
+ ByteBuf buffer = convertToByteBuf(inputStream);
+
+ ByteBuf firstLine = readLine(buffer);
+ MultiValueMap headers = decodeHeaders(buffer);
+ ByteBuf inboundBytes = buffer.readBytes(buffer.readableBytes());
+
+ return initialize(firstLine, headers, inboundBytes);
+ }
+
+ private ByteBuf convertToByteBuf(InputStream inputStream) throws IOException {
+ ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
+ buffer.writeBytes(inputStream, MAX_BUFFER_SIZE);
+ return buffer;
+ }
+
+ protected MultiValueMap decodeHeaders(ByteBuf buffer) {
+ MultiValueMap headers = new LinkedMultiValueMap<>();
+
+ while (buffer.isReadable() && isEndOfHeader(buffer)) {
+ ByteBuf headerLine = readLine(buffer);
+ String key = lineSplitter.slice(headerLine, ch -> ch == ':', false);
+ String value = lineSplitter.slice(headerLine, ch -> ch == '\n', true);
+ headers.add(key, value);
+ }
+ return headers;
+ }
+
+ protected abstract T initialize(ByteBuf firstLine, MultiValueMap headers, ByteBuf inboundBytes);
+
+ protected ByteBuf readLine(ByteBuf byteBuffer) {
+ return readSlice(byteBuffer, (byte)'\n');
+ }
+
+ protected ByteBuf readSlice(ByteBuf byteBuffer, byte delimiter) {
+ int index = byteBuffer.bytesBefore(delimiter);
+ return byteBuffer.readSlice(index + 1);
+ }
+
+ protected ByteBuffer getBytes(ByteBuf inboundBytes) {
+ int length = inboundBytes.readableBytes();
+ ByteBuffer contnet = inboundBytes.readBytes(length)
+ .nioBuffer();
+ inboundBytes.release();
+ return contnet;
+ }
+
+ private boolean isEndOfHeader(ByteBuf buffer) {
+ if (buffer.getByte(buffer.readerIndex()) == 13) {
+ buffer.skipBytes(2);
+ return false;
+ }
+ return true;
+ }
+
+ class LineSplitter {
+ private final AppendableCharSequence header;
+
+ public LineSplitter(AppendableCharSequence header) {
+ this.header = header;
+ }
+
+ private boolean isOWS(char character) {
+ return character == ' ' || character == '\t';
+ }
+
+ private void skipOWS() {
+ int length = header.length() - 1;
+ for (int index = length; index >= 0; index--) {
+ if (!isOWS(header.charAtUnsafe(index))) {
+ header.setLength(index + 1);
+ break;
+ }
+ }
+ }
+
+ public List split(ByteBuf buffer) {
+ List fragments = Lists.newArrayList();
+ while (buffer.isReadable()) {
+ fragments.add(slice(buffer, ch -> ch == ' ' || ch == '\t' || ch == '\n', false));
+ }
+ return fragments;
+ }
+
+ public String slice(ByteBuf buffer, Predicate predicate, boolean canTrim) {
+ header.reset();
+ int index = buffer.forEachByte(value -> {
+ char next = (char)value;
+ int length = header.length();
+
+ if (predicate.test(next)) {
+ if (header.length() > 0 && header.charAtUnsafe(length - 1) == '\r') {
+ header.setLength(length - 1);
+ }
+ if (canTrim) {
+ skipOWS();
+ }
+ return false;
+ }
+
+ if (canTrim && header.length() == 0 && isOWS(next)) {
+ return true;
+ }
+ header.append(next);
+ return true;
+ });
+
+ if (index == -1) {
+ index += (buffer.readerIndex() + buffer.readableBytes());
+ }
+ buffer.readerIndex(index + 1);
+ return header.toString();
+ }
+ }
+}
diff --git a/src/main/java/webserver/http/HttpMethod.java b/src/main/java/webserver/http/HttpMethod.java
new file mode 100644
index 000000000..a079a02c8
--- /dev/null
+++ b/src/main/java/webserver/http/HttpMethod.java
@@ -0,0 +1,9 @@
+package webserver.http;
+
+public enum HttpMethod {
+ GET, POST;
+
+ public static HttpMethod of(String method) {
+ return HttpMethod.valueOf(method.toUpperCase());
+ }
+}
diff --git a/src/main/java/webserver/http/HttpRequest.java b/src/main/java/webserver/http/HttpRequest.java
new file mode 100644
index 000000000..cd5c0453d
--- /dev/null
+++ b/src/main/java/webserver/http/HttpRequest.java
@@ -0,0 +1,28 @@
+package webserver.http;
+
+import java.util.Map;
+
+import org.springframework.util.MultiValueMap;
+
+public interface HttpRequest extends HttpMessage {
+ MultiValueMap getQueryParams();
+
+ HttpMethod getMethod();
+
+ String getParameter(String name);
+
+ String getRequestUri();
+
+ String getServletPath();
+ String getQueryString();
+
+ void setAttribute(String name, Object value);
+
+ Object getAttribute(String name);
+
+ Map getCookies();
+
+ HttpSession getSession();
+
+ String getRequestedSessionId();
+}
diff --git a/src/main/java/webserver/http/HttpRequestDecoder.java b/src/main/java/webserver/http/HttpRequestDecoder.java
new file mode 100644
index 000000000..8cb3d9b10
--- /dev/null
+++ b/src/main/java/webserver/http/HttpRequestDecoder.java
@@ -0,0 +1,48 @@
+package webserver.http;
+
+import java.net.URI;
+import java.util.List;
+
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.util.MultiValueMap;
+
+import io.netty.buffer.ByteBuf;
+
+public class HttpRequestDecoder extends HttpMessageDecoder {
+ private static final int HTTP_METHOD_INDEX = 0;
+ private static final int HTTP_URI_INDEX = 1;
+ private static final int HTTP_VERSION_INDEX = 2;
+ private static final int LINE_ELEMENT_SIZE = 3;
+
+ @Override
+ protected HttpRequest initialize(ByteBuf firstLine, MultiValueMap headers, ByteBuf inboundBytes) {
+
+ return new DefaultRequest(decodeFirstLine(firstLine), headers, getBytes(inboundBytes));
+ }
+
+ protected RequestLine decodeFirstLine(ByteBuf buffer) {
+ List requestLine = lineSplitter.split(buffer);
+ if (hasBlank(requestLine)) {
+ throw new IllegalArgumentException("Invalid separator. only a single space or horizontal tab allowed.");
+ }
+
+ if (requestLine.size() != LINE_ELEMENT_SIZE) {
+ throw new IllegalArgumentException(
+ "Invalid argument count exception. There must be 3 elements, but received (" + requestLine.size()
+ + ")");
+ }
+
+ HttpMethod method = HttpMethod.of(requestLine.get(HTTP_METHOD_INDEX));
+ URI uri = URI.create(requestLine.get(HTTP_URI_INDEX));
+ HttpVersion httpVersion = HttpVersion.of(findElement(requestLine, HTTP_VERSION_INDEX));
+ return new RequestLine(method, uri, httpVersion);
+ }
+ private String findElement(List requestLine, int index) {
+ return requestLine.get(index);
+ }
+
+ private boolean hasBlank(List requestLine) {
+ return requestLine.stream()
+ .anyMatch(Strings::isBlank);
+ }
+}
diff --git a/src/main/java/webserver/http/HttpResponse.java b/src/main/java/webserver/http/HttpResponse.java
new file mode 100644
index 000000000..d1fa17e62
--- /dev/null
+++ b/src/main/java/webserver/http/HttpResponse.java
@@ -0,0 +1,94 @@
+package webserver.http;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+public class HttpResponse implements HttpMessage {
+ private HttpStatus httpStatus;
+ private HttpVersion httpVersion;
+ private MultiValueMap headers;
+ private final List cookies = new ArrayList<>();
+ private final DataOutputStream writer;
+
+ public HttpResponse(OutputStream outputStream) {
+ this.writer = new DataOutputStream(outputStream);
+ }
+
+ public DataOutputStream getWriter() {
+ return writer;
+ }
+
+ public void sendRedirect(String location) throws IOException {
+ setHttpStatus(HttpStatus.FOUND);
+ addHeader(LOCATION, location);
+ writer.writeBytes(toEncoded());
+ writer.close();
+ }
+
+ public void sendError(HttpStatus sendError) throws IOException {
+ setHttpStatus(sendError);
+ writer.writeBytes(toEncoded());
+ writer.close();
+ }
+
+ public void addCookie(Cookie cookie) {
+ cookies.add(cookie);
+ addHeader(SET_COOKIE, cookie.toEncoded());
+ }
+
+ public void setHttpStatus(HttpStatus httpStatus) {
+ this.httpStatus = httpStatus;
+ }
+
+ @Override
+ public void addHeader(String key, String value) {
+ if (headers == null) {
+ headers = new LinkedMultiValueMap<>();
+ }
+ headers.add(key, value);
+ }
+
+ @Override
+ public String getHeader(String key) {
+ return headers.getFirst(key);
+ }
+
+ @Override
+ public MultiValueMap getHeaders() {
+ return this.headers;
+ }
+
+ @Override
+ public void setHttpVersion(HttpVersion httpVersion) {
+ this.httpVersion = httpVersion;
+ }
+
+ @Override
+ public HttpVersion getHttpVersion() {
+ return this.httpVersion;
+ }
+
+ public String toEncoded() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("HTTP/1.1")
+ .append(" ")
+ .append(httpStatus.getValue())
+ .append(" ")
+ .append(httpStatus.getReasonPhrase())
+ .append("\r\n");
+ if (headers != null) {
+ headers.forEach((key, values) -> values.forEach(value -> builder.append(key)
+ .append(": ")
+ .append(value)
+ .append("\r\n")));
+ }
+ builder.append("\r\n");
+ return builder.toString();
+ }
+}
diff --git a/src/main/java/webserver/http/HttpSession.java b/src/main/java/webserver/http/HttpSession.java
new file mode 100644
index 000000000..2533ccdec
--- /dev/null
+++ b/src/main/java/webserver/http/HttpSession.java
@@ -0,0 +1,60 @@
+package webserver.http;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class HttpSession {
+ public static final String SESSION_ID = "JSESSIONID";
+ private String sessionId;
+ private volatile boolean changed;
+ private final Map attributes = new ConcurrentHashMap<>();
+
+ public HttpSession(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ public void setAttribute(String key, Object value) {
+ attributes.put(key, value);
+ }
+
+ public Object getAttribute(String key) {
+ return attributes.get(key);
+ }
+
+ public void removeAttribute(String key) {
+ attributes.remove(key);
+ }
+
+ public void invalidate() {
+ sessionId = null;
+ }
+
+ public boolean setChanged(boolean changed) {
+ this.changed = changed;
+ return true;
+ }
+
+ public boolean hasChanged() {
+ return changed;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ HttpSession that = (HttpSession)o;
+ return Objects.equals(sessionId, that.sessionId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(sessionId);
+ }
+}
diff --git a/src/main/java/webserver/http/HttpStatus.java b/src/main/java/webserver/http/HttpStatus.java
new file mode 100644
index 000000000..9e5bac024
--- /dev/null
+++ b/src/main/java/webserver/http/HttpStatus.java
@@ -0,0 +1,22 @@
+package webserver.http;
+
+public enum HttpStatus {
+ OK(200, "OK"),
+ FOUND(302, "Found"),
+ NOT_FOUND(404, "Not Found");
+ private final int value;
+ private final String reasonPhrase;
+
+ HttpStatus(int value, String reasonPhrase) {
+ this.value = value;
+ this.reasonPhrase = reasonPhrase;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public String getReasonPhrase() {
+ return reasonPhrase;
+ }
+}
diff --git a/src/main/java/webserver/HttpVersion.java b/src/main/java/webserver/http/HttpVersion.java
similarity index 93%
rename from src/main/java/webserver/HttpVersion.java
rename to src/main/java/webserver/http/HttpVersion.java
index 2ffd885d8..96c24eee4 100644
--- a/src/main/java/webserver/HttpVersion.java
+++ b/src/main/java/webserver/http/HttpVersion.java
@@ -1,4 +1,4 @@
-package webserver;
+package webserver.http;
import org.springframework.util.Assert;
@@ -20,16 +20,14 @@ public HttpVersion(String protocolName, int majorVersion, int minorVersion, bool
this.rawVersion = rawVersion;
}
- public static HttpVersion valueOf(String version) {
+ public static HttpVersion of(String version) {
Assert.notNull(version.trim(), "version must not be null");
-
if(HTTP_1_0.isSameAs(version)) {
return HTTP_1_0;
}
if(HTTP_1_1.isSameAs(version)) {
return HTTP_1_1;
}
-
throw new IllegalArgumentException("Unsupported HTTP version.");
}
diff --git a/src/main/java/webserver/http/QueryStringDecoder.java b/src/main/java/webserver/http/QueryStringDecoder.java
new file mode 100644
index 000000000..0d58104b5
--- /dev/null
+++ b/src/main/java/webserver/http/QueryStringDecoder.java
@@ -0,0 +1,60 @@
+package webserver.http;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collector;
+
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import com.google.common.base.Splitter;
+
+public class QueryStringDecoder {
+ public static MultiValueMap parseQueryString(ByteBuffer content) {
+ String queryString = new String(getBytes(content));
+ return parseQueryString(queryString);
+ }
+
+ public static MultiValueMap parseQueryString(String queryString) {
+ if (Strings.isBlank(queryString)) {
+ return new LinkedMultiValueMap<>();
+ }
+ return Splitter.on("&")
+ .splitToStream(queryString)
+ .map(entry -> Splitter.on("=")
+ .omitEmptyStrings()
+ .trimResults()
+ .split(entry))
+ .collect(Collector.of(LinkedMultiValueMap::new
+ , accumulate, combine));
+ }
+
+ public static BiConsumer, Iterable> accumulate = (map, entryFields) -> {
+ Iterator iterator = entryFields.iterator();
+ if (iterator.hasNext()) {
+ String key = iterator.next();
+ String value = "";
+ if (iterator.hasNext()) {
+ value = iterator.next();
+ }
+ map.add(key, value);
+ }
+ };
+
+ public static BinaryOperator> combine = (oldMap, newMap) -> {
+ oldMap.addAll(newMap);
+ return oldMap;
+ };
+
+ private static byte[] getBytes(ByteBuffer content) {
+ if (content.hasArray()) {
+ return content.array();
+ }
+ byte[] buffer = new byte[content.remaining()];
+ content.get(buffer);
+ return buffer;
+ }
+}
diff --git a/src/main/java/webserver/http/RequestLine.java b/src/main/java/webserver/http/RequestLine.java
new file mode 100644
index 000000000..ca90b8524
--- /dev/null
+++ b/src/main/java/webserver/http/RequestLine.java
@@ -0,0 +1,32 @@
+package webserver.http;
+
+import java.net.URI;
+
+public class RequestLine {
+ private HttpMethod method;
+ private URI uri;
+
+ private String queryString;
+ private HttpVersion httpVersion;
+ public RequestLine(HttpMethod method, URI uri, HttpVersion httpVersion) {
+ this.method = method;
+ this.uri = uri;
+ this.queryString = uri.getQuery();
+ this.httpVersion = httpVersion;
+ }
+ public HttpMethod getMethod() {
+ return method;
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ public String getQueryString() {
+ return queryString;
+ }
+
+ public HttpVersion getHttpVersion() {
+ return httpVersion;
+ }
+}
diff --git a/src/main/java/webserver/http/SessionManager.java b/src/main/java/webserver/http/SessionManager.java
new file mode 100644
index 000000000..e1ef708fb
--- /dev/null
+++ b/src/main/java/webserver/http/SessionManager.java
@@ -0,0 +1,37 @@
+package webserver.http;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class SessionManager {
+ private static final SessionManager instance = new SessionManager();
+
+ private final Map sessions = new ConcurrentHashMap<>();
+
+ public static SessionManager getInstance() {
+ return instance;
+ }
+
+ public HttpSession findSession(String sessionId) {
+ return sessions.get(sessionId);
+ }
+
+ public void removeSession(String sessionId) {
+ HttpSession session = sessions.get(sessionId);
+ session.invalidate();
+ sessions.remove(sessionId);
+ }
+
+ public boolean isSessionIdValid(String sessionId) {
+ if (sessionId == null) {
+ return true;
+ }
+ return sessions.containsKey(sessionId);
+ }
+
+ public HttpSession createSession(String sessionId) {
+ HttpSession session = new HttpSession(sessionId);
+ sessions.put(sessionId, session);
+ return session;
+ }
+}
diff --git a/src/main/java/webserver/servlet/AbstractServlet.java b/src/main/java/webserver/servlet/AbstractServlet.java
new file mode 100644
index 000000000..bbf1a7288
--- /dev/null
+++ b/src/main/java/webserver/servlet/AbstractServlet.java
@@ -0,0 +1,36 @@
+package webserver.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import webserver.http.Cookie;
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+import webserver.http.HttpSession;
+import webserver.http.HttpStatus;
+
+public abstract class AbstractServlet implements Servlet {
+ private static final Logger logger = LoggerFactory.getLogger(AbstractServlet.class);
+ public void service(HttpRequest request, HttpResponse response) throws IOException {
+ HttpSession session = request.getSession();
+ if (session.hasChanged() && session.setChanged(false)) {
+ Cookie cookie = new Cookie(HttpSession.SESSION_ID, session.getSessionId());
+ cookie.setCookieMaxAge(60);
+ response.addCookie(cookie);
+ }
+
+ response.setHttpVersion(request.getHttpVersion());
+ response.setHttpStatus(HttpStatus.OK);
+
+ try {
+ doService(request, response);
+ } catch (FileNotFoundException exception) {
+ response.sendError(HttpStatus.NOT_FOUND);
+ logger.warn(exception.getMessage());
+ }
+ }
+ protected abstract void doService(HttpRequest request, HttpResponse response) throws IOException;
+}
\ No newline at end of file
diff --git a/src/main/java/webserver/servlet/DefaultServlet.java b/src/main/java/webserver/servlet/DefaultServlet.java
new file mode 100644
index 000000000..790e6e8a5
--- /dev/null
+++ b/src/main/java/webserver/servlet/DefaultServlet.java
@@ -0,0 +1,91 @@
+package webserver.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+
+import utils.FileIoUtils;
+import webserver.http.HttpMessage;
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+
+public class DefaultServlet extends AbstractServlet {
+
+ public void doService(HttpRequest request, HttpResponse response) throws IOException {
+ String requestUri = getRequestPath(request);
+
+ ResourceType resourceType = ResourceType.of(requestUri);
+
+ byte[] content = loadResource(getResourcePath(requestUri, resourceType));
+ int contentLength = content.length;
+
+ response.addHeader(HttpMessage.CONTENT_LENGTH, String.valueOf(contentLength));
+ response.addHeader(HttpMessage.CONTENT_TYPE, resourceType.getContentType());
+
+ response.getWriter().writeBytes(response.toEncoded());
+ response.getWriter().write(content, 0, contentLength);
+ response.getWriter().flush();
+ response.getWriter().close();
+ }
+
+ private byte[] loadResource(String resource) throws FileNotFoundException {
+ try {
+ return FileIoUtils.loadFileFromClasspath(resource);
+ } catch (Exception exception) {
+ throw new FileNotFoundException("The specified path does not exist in the classpath. [" + resource + "]");
+ }
+ }
+ private String getRequestPath(HttpRequest request) {
+ String requestUri = request.getRequestUri();
+
+ if (requestUri.equals("/")) {
+ requestUri = "/index.html";
+ }
+ return requestUri;
+ }
+
+ private String getResourcePath(String requestUri, ResourceType resourceType) {
+ return resourceType.getResourceRootPath() + requestUri;
+ }
+
+ enum ResourceType {
+ HTML("text/html", ".html", ResourceType.TEMPLATES),
+ JS("application/javascript;charset=utf-8", ".js", ResourceType.STATIC),
+ CSS("text/css", ".css", ResourceType.STATIC),
+ ICO("image/x-icon", ".ico", ResourceType.STATIC),
+ PNG("image/png", ".png", ResourceType.STATIC),
+ JPG("image/jpeg", ".jpg", ResourceType.STATIC),
+ UNKNOWN("text/html", "", ResourceType.STATIC);
+
+ private static final String TEMPLATES = "templates";
+ public static final String STATIC = "static";
+ private String contentType;
+ private String extension;
+ private String resourceRootPath;
+
+ ResourceType(String contentType, String extension, String resourceRootPath) {
+ this.contentType = contentType;
+ this.extension = extension;
+ this.resourceRootPath = resourceRootPath;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public String getExtension() {
+ return extension;
+ }
+
+ public String getResourceRootPath() {
+ return resourceRootPath;
+ }
+
+ public static ResourceType of(String requestUri) {
+ return Arrays.stream(values())
+ .filter(resourceType -> requestUri.endsWith(resourceType.getExtension()))
+ .findFirst()
+ .orElse(UNKNOWN);
+ }
+ }
+}
diff --git a/src/main/java/webserver/servlet/Servlet.java b/src/main/java/webserver/servlet/Servlet.java
new file mode 100644
index 000000000..b0ecaeebc
--- /dev/null
+++ b/src/main/java/webserver/servlet/Servlet.java
@@ -0,0 +1,10 @@
+package webserver.servlet;
+
+import java.io.IOException;
+
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+
+public interface Servlet {
+ void service(HttpRequest request, HttpResponse response) throws IOException;
+}
diff --git a/src/main/java/webserver/servlet/ServletMapping.java b/src/main/java/webserver/servlet/ServletMapping.java
new file mode 100644
index 000000000..eb451a926
--- /dev/null
+++ b/src/main/java/webserver/servlet/ServletMapping.java
@@ -0,0 +1,19 @@
+package webserver.servlet;
+
+import java.util.Map;
+
+public class ServletMapping {
+ private ServletMapping() {
+ throw new IllegalStateException("This is utility class for servlet mapping");
+ }
+
+ private static final Map servlets = Map.of(
+ "/user/create", new UserCreateServlet(),
+ "/user/login", new UserLoginServlet(),
+ "/user/list", new UserListServlet()
+ );
+
+ public static Servlet match(String servletPath) {
+ return servlets.getOrDefault(servletPath, new DefaultServlet());
+ }
+}
diff --git a/src/main/java/webserver/servlet/UserCreateServlet.java b/src/main/java/webserver/servlet/UserCreateServlet.java
new file mode 100644
index 000000000..2bf385e67
--- /dev/null
+++ b/src/main/java/webserver/servlet/UserCreateServlet.java
@@ -0,0 +1,22 @@
+package webserver.servlet;
+
+import java.io.IOException;
+
+import db.DataBase;
+import model.User;
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+
+public class UserCreateServlet extends AbstractServlet {
+ public void doService(HttpRequest request, HttpResponse response) throws IOException {
+ String userId = request.getParameter("userId");
+ String password = request.getParameter("password");
+ String name = request.getParameter("name");
+ String email = request.getParameter("email");
+
+ User user = new User(userId, password, name, email);
+ DataBase.addUser(user);
+
+ response.sendRedirect("/");
+ }
+}
diff --git a/src/main/java/webserver/servlet/UserListServlet.java b/src/main/java/webserver/servlet/UserListServlet.java
new file mode 100644
index 000000000..ea358dfcb
--- /dev/null
+++ b/src/main/java/webserver/servlet/UserListServlet.java
@@ -0,0 +1,50 @@
+package webserver.servlet;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import com.github.jknack.handlebars.Handlebars;
+import com.github.jknack.handlebars.Template;
+import com.github.jknack.handlebars.io.ClassPathTemplateLoader;
+import com.github.jknack.handlebars.io.TemplateLoader;
+
+import db.DataBase;
+import webserver.http.HttpMessage;
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+import webserver.http.HttpSession;
+
+public class UserListServlet extends AbstractServlet {
+ @Override
+ public void doService(HttpRequest request, HttpResponse response) throws IOException {
+ if (!hasAuthorization(request)) {
+ response.sendRedirect("/user/login.html");
+ return;
+ }
+
+ TemplateLoader loader = new ClassPathTemplateLoader();
+ loader.setPrefix("/templates");
+ loader.setSuffix(".html");
+
+ Handlebars handlebars = new Handlebars(loader);
+
+ Template template = handlebars.registerHelper("inc",
+ (context, options) -> Integer.parseInt(context.toString()) + 3)
+ .compile("/user/list");
+
+ byte[] content = template.apply(DataBase.findAll()).getBytes();
+ int length = content.length;
+
+ response.addHeader(HttpMessage.CONTENT_TYPE, "text/html;charset=utf-8");
+ response.addHeader(HttpMessage.CONTENT_LENGTH, String.valueOf(length));
+ response.getWriter().writeBytes(response.toEncoded());
+ response.getWriter().write(content, 0, length);
+ response.getWriter().flush();
+ response.getWriter().close();
+ }
+
+ private boolean hasAuthorization(HttpRequest request) {
+ HttpSession session = request.getSession();
+ return Objects.nonNull(session.getAttribute("user"));
+ }
+}
diff --git a/src/main/java/webserver/servlet/UserLoginServlet.java b/src/main/java/webserver/servlet/UserLoginServlet.java
new file mode 100644
index 000000000..66a313753
--- /dev/null
+++ b/src/main/java/webserver/servlet/UserLoginServlet.java
@@ -0,0 +1,28 @@
+package webserver.servlet;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import db.DataBase;
+import model.User;
+import webserver.http.HttpRequest;
+import webserver.http.HttpResponse;
+import webserver.http.HttpSession;
+public class UserLoginServlet extends AbstractServlet {
+ public void doService(HttpRequest request, HttpResponse response) throws IOException {
+ String userId = request.getParameter("userId");
+ String password = request.getParameter("password");
+ String location = "/index.html";
+
+ User user = DataBase.findUserById(userId);
+
+ if (Objects.nonNull(user) && user.matchPassword(password)) {
+ HttpSession session = request.getSession();
+ session.setAttribute("user", user);
+ } else {
+ location = "/user/login_failed.html";
+ }
+
+ response.sendRedirect(location);
+ }
+}
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index 1675898ad..e9ec82d5d 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -40,7 +40,7 @@
Facebook
-
+
diff --git a/src/main/resources/templates/user/list.html b/src/main/resources/templates/user/list.html
index 3ff40952f..808cbd768 100644
--- a/src/main/resources/templates/user/list.html
+++ b/src/main/resources/templates/user/list.html
@@ -89,6 +89,11 @@
| 2 | slipp | 슬립 | slipp@sample.net | 수정 |
+ {{#each}}
+
+ | {{inc @key}} | {{userId}} | {{name}} | {{email}} | 수정 |
+
+ {{/each}}
diff --git a/src/test/java/webserver/HttpRequestTest.java b/src/test/java/webserver/HttpRequestTest.java
deleted file mode 100644
index 73bc72794..000000000
--- a/src/test/java/webserver/HttpRequestTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package webserver;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestTemplate;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class HttpRequestTest {
- @Test
- void request_resttemplate() {
- RestTemplate restTemplate = new RestTemplate();
- ResponseEntity response = restTemplate.getForEntity("http://localhost:8080", String.class);
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
- }
-}
diff --git a/src/test/java/webserver/RequestLineDecodeTest.java b/src/test/java/webserver/RequestLineDecodeTest.java
deleted file mode 100644
index 2edb656d4..000000000
--- a/src/test/java/webserver/RequestLineDecodeTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package webserver;
-
-import java.nio.charset.StandardCharsets;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import io.netty.buffer.Unpooled;
-
-public class RequestLineDecodeTest {
- static RequestLineDecoder requestLineDecoder;
- @BeforeAll
- public static void setUp() {
- requestLineDecoder = new RequestLineDecoder();
- }
- @Test
- public void decodeRequestLine() {
- byte[] requestLine = "GET /hello?page=1&category=admin HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8);
-
- RequestLine httpRequestLine = requestLineDecoder.decode(Unpooled.wrappedBuffer(requestLine));
-
- Assertions.assertEquals("GET", httpRequestLine.getMethod());
- Assertions.assertEquals(HttpVersion.HTTP_1_1, httpRequestLine.getHttpVersion());
- Assertions.assertEquals("/hello", httpRequestLine.getUri().getPath());
- Assertions.assertEquals("admin", httpRequestLine.getUri().getParams().get("category"));
- }
-
-
- @Test
- void when_lessThanThreeElements_thenRequestLineFormatExceptionThrown(){
- byte[] requestLine = "GET /hello\r\n".getBytes(StandardCharsets.UTF_8);
-
- Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
- RequestLine httpRequestLine = requestLineDecoder.decode(Unpooled.wrappedBuffer(requestLine));
- });
-
- String expectedMessage = "Invalid argument count exception. There must be 3 elements";
- String actualMessage = exception.getMessage();
-
- Assertions.assertTrue(actualMessage.contains(expectedMessage));
- }
-
- @Test
- void when_moreThanOneSeparator_thenRequestLineFormatExceptionThrown(){
- byte[] requestLine = "GET /hello HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8);
-
- Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
- RequestLine httpRequestLine = requestLineDecoder.decode(Unpooled.wrappedBuffer(requestLine));
- });
-
- String expectedMessage = "Invalid separator. only a single space or horizontal tab allowed.";
- String actualMessage = exception.getMessage();
-
- Assertions.assertTrue(actualMessage.contains(expectedMessage));
- }
-
- @Test
- void when_wrongUriForamt_thenUriFormatExceptionThrown(){
- byte[] requestLine = "GET hello HTTP/1.1\r\n".getBytes(StandardCharsets.UTF_8);
-
- Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
- RequestLine httpRequestLine = requestLineDecoder.decode(Unpooled.wrappedBuffer(requestLine));
- });
-
- String expectedMessage = "Unsupported URI format. URI must start with '/'";
- String actualMessage = exception.getMessage();
-
- Assertions.assertTrue(actualMessage.contains(expectedMessage));
- }
-}
\ No newline at end of file
diff --git a/src/test/java/webserver/http/DefaultRequestTest.java b/src/test/java/webserver/http/DefaultRequestTest.java
new file mode 100644
index 000000000..3eb0485f8
--- /dev/null
+++ b/src/test/java/webserver/http/DefaultRequestTest.java
@@ -0,0 +1,66 @@
+package webserver.http;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class DefaultRequestTest {
+ HttpMessageGenerator messageGenerator;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ messageGenerator = new HttpMessageGenerator();
+ }
+
+ @DisplayName("When Request is a POST type, queryString can be parsed.")
+ @Test
+ void whenPostTypeQueryStringShouldParsed() throws IOException {
+ DefaultRequest request = (DefaultRequest)messageGenerator.generatePostRequest();
+
+ assertThat(request.isFormRequest()).isTrue();
+ assertThat(request.getParameter("key")).isEqualTo("value");
+ assertThat(request.getParameter("page")).isEqualTo("1");
+ }
+
+ @DisplayName("When Request is a GET type, queryString can be parsed.")
+ @Test
+ void whenGetTypeQueryStringShouldParsed() throws IOException {
+ DefaultRequest request = (DefaultRequest)messageGenerator.generateGetRequest();
+
+ assertThat(request.isFormRequest()).isFalse();
+ assertThat(request.getParameter("key")).isEqualTo("value");
+ assertThat(request.getParameter("page")).isEqualTo("1");
+ }
+
+ @Test
+ void parseCookies() throws IOException {
+ DefaultRequest request = (DefaultRequest)messageGenerator.generateGetRequest();
+
+ assertThat(request.getCookies())
+ .containsValue(new Cookie("JSESSIONID", "50f59aa2-3d3b-451c-9aaa-9b5d5d5db6d5"))
+ .containsValue(new Cookie("utma", "9384732"));
+ }
+
+ @Test
+ void parseSession() throws IOException {
+ String sessionId = "50f59aa2-3d3b-451c-9aaa-9b5d5d5db6d5";
+ DefaultRequest request = (DefaultRequest)messageGenerator.generateGetRequest();
+
+ HttpSession session = request.getSession();
+ assertThat(session.hasChanged()).isTrue();
+ assertThat(session.getSessionId()).isEqualTo(sessionId);
+ }
+
+ @Test
+ void parseFirstLine() throws IOException {
+ DefaultRequest request = (DefaultRequest)messageGenerator.generateGetRequest();
+
+ assertThat(request.getRequestUri()).isEqualTo("/hello/index.html");
+ assertThat(request.getServletPath()).isEqualTo("/hello");
+ assertThat(request.getQueryString()).isEqualTo("key=value&page=1");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/webserver/http/HttpMessageGenerator.java b/src/test/java/webserver/http/HttpMessageGenerator.java
new file mode 100644
index 000000000..50258b25d
--- /dev/null
+++ b/src/test/java/webserver/http/HttpMessageGenerator.java
@@ -0,0 +1,51 @@
+package webserver.http;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.springframework.util.MultiValueMap;
+
+import io.netty.buffer.Unpooled;
+
+public class HttpMessageGenerator {
+ private String get = "GET /hello/index.html?key=value&page=1 HTTP/1.1\r\n"
+ + "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)\r\n"
+ + "Host: www.tutorialspoint.com\r\n"
+ + "Cookie: JSESSIONID=50f59aa2-3d3b-451c-9aaa-9b5d5d5db6d5; utma=9384732\r\n"
+ + "Accept-Language: en-us\r\n"
+ + "Accept-Encoding: gzip, deflate\r\n"
+ + "Connection: Keep-Alive\r\n"
+ + "\r\n";
+
+ private String post = "POST /hello.html HTTP/1.1\r\n"
+ + "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)\r\n"
+ + "Host: www.tutorialspoint.com\r\n"
+ + "Accept-Language: en-us\r\n"
+ + "Accept-Encoding: gzip, deflate\r\n"
+ + "Connection: Keep-Alive\r\n"
+ + "Content-Type: application/x-www-form-urlencoded\r\n"
+ + "Content-Length: 27\r\n"
+ + "\r\n"
+ + "key=value&page=1&name=abc\r\n";
+
+ HttpRequest generateGetRequest() throws IOException {
+ HttpRequestDecoder requestDecoder = new HttpRequestDecoder();
+ return requestDecoder.decode(new ByteArrayInputStream(get.getBytes()));
+ }
+
+ HttpRequest generatePostRequest() throws IOException {
+ HttpRequestDecoder requestDecoder = new HttpRequestDecoder();
+ return requestDecoder.decode(new ByteArrayInputStream(post.getBytes()));
+ }
+
+ RequestLine generateRequestLine(String requestLine) {
+ HttpRequestDecoder requestDecoder = new HttpRequestDecoder();
+ return requestDecoder.decodeFirstLine(
+ Unpooled.wrappedBuffer(requestLine.getBytes()));
+ }
+
+ MultiValueMap generateHeaders(){
+ HttpRequestDecoder requestDecoder = new HttpRequestDecoder();
+ return requestDecoder.decodeHeaders(Unpooled.wrappedBuffer(get.getBytes()));
+ }
+}
diff --git a/src/test/java/webserver/http/HttpRequestDecoderTest.java b/src/test/java/webserver/http/HttpRequestDecoderTest.java
new file mode 100644
index 000000000..8d56237d2
--- /dev/null
+++ b/src/test/java/webserver/http/HttpRequestDecoderTest.java
@@ -0,0 +1,79 @@
+package webserver.http;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.params.provider.Arguments.*;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.util.MultiValueMap;
+
+class HttpRequestDecoderTest {
+ HttpMessageGenerator messageGenerator;
+
+ @BeforeEach
+ void setUp() {
+ messageGenerator = new HttpMessageGenerator();
+ }
+
+ @DisplayName("request line uses only one ows as a delimiter.")
+ @Test
+ void throwsExceptionWhenNotSingleOWS() {
+ String expectedMessage = "Invalid separator. only a single space or horizontal tab allowed.";
+ Class expectedException = IllegalArgumentException.class;
+
+ String message = "POST /hello.html?key=value&page=1 HTTP/1.1\r\n";
+
+ assertThatThrownBy(() -> {
+ messageGenerator.generateRequestLine(message);
+ }).isInstanceOf(expectedException)
+ .hasMessage(expectedMessage);
+ }
+
+ @DisplayName("request line should consist of three elements.")
+ @Test
+ void throwsExceptionWhenBadRequestLine() {
+ String expectedMessage = "Invalid argument count exception. There must be 3 elements, but received (2)";
+ Class expectedException = IllegalArgumentException.class;
+
+ String message = "POST/hello.html?key=value&page=1 HTTP/1.1\r\n";
+
+ assertThatThrownBy(() -> {
+ messageGenerator.generateRequestLine(message);
+ }).isInstanceOf(expectedException)
+ .hasMessage(expectedMessage);
+ }
+
+ @Test
+ void decodeRequestLine() {
+ String message = "GET /hello.html?key=value&page=1 HTTP/1.1\r\n";
+ RequestLine requestLine = messageGenerator.generateRequestLine(message);
+
+ assertThat(requestLine.getHttpVersion()).isEqualTo(HttpVersion.HTTP_1_1);
+ assertThat(requestLine.getMethod()).isEqualTo(HttpMethod.GET);
+ assertThat(requestLine.getUri().getPath()).isEqualTo("/hello.html");
+ assertThat(requestLine.getUri().getQuery()).isEqualTo("key=value&page=1");
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideArguments")
+ void decodeHeaders(String key, String value) throws IOException {
+ MultiValueMap headers = messageGenerator.generateHeaders();
+
+ assertThat(headers.getFirst(key)).isEqualTo(value);
+ }
+
+ private static Stream provideArguments() {
+ return Stream.of(
+ arguments("User-Agent", "Mozilla/4.0 (compatible; MSIE5.01; Windows NT)"),
+ arguments("Cookie", "JSESSIONID=50f59aa2-3d3b-451c-9aaa-9b5d5d5db6d5; utma=9384732"),
+ arguments("Accept-Language", "en-us")
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/webserver/http/HttpRequestTest.java b/src/test/java/webserver/http/HttpRequestTest.java
new file mode 100644
index 000000000..ded92a96d
--- /dev/null
+++ b/src/test/java/webserver/http/HttpRequestTest.java
@@ -0,0 +1,17 @@
+package webserver.http;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+class HttpRequestTest {
+ @Test
+ void request_resttemplate() {
+ RestTemplate restTemplate = new RestTemplate();
+ ResponseEntity response = restTemplate.getForEntity("http://localhost:8080", String.class);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/webserver/http/HttpResponseTest.java b/src/test/java/webserver/http/HttpResponseTest.java
new file mode 100644
index 000000000..616cfa4e7
--- /dev/null
+++ b/src/test/java/webserver/http/HttpResponseTest.java
@@ -0,0 +1,25 @@
+package webserver.http;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.junit.jupiter.api.Test;
+
+public class HttpResponseTest {
+ @Test
+ void sendRedirectTest() throws IOException {
+ String expected = "HTTP/1.1 302 Found\r\n" +
+ "Location: /index.html\r\n" +
+ "\r\n";
+ String location = "/index.html";
+
+ OutputStream outputStream = new ByteArrayOutputStream();
+ HttpResponse httpResponse = new HttpResponse(outputStream);
+ httpResponse.sendRedirect(location);
+
+ assertThat(httpResponse.toEncoded()).isEqualTo(expected);
+ }
+}
diff --git a/src/test/java/webserver/http/QueryStringDecoderTest.java b/src/test/java/webserver/http/QueryStringDecoderTest.java
new file mode 100644
index 000000000..f9f01ead7
--- /dev/null
+++ b/src/test/java/webserver/http/QueryStringDecoderTest.java
@@ -0,0 +1,31 @@
+package webserver.http;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.springframework.util.MultiValueMap;
+
+class QueryStringDecoderTest {
+ @Test
+ void parseQueryString() {
+ String queryString = "name=alice&age=20&name=bob&age=30&page=1\r\n";
+ QueryStringDecoder decoder = new QueryStringDecoder();
+
+ MultiValueMap parameters = decoder.parseQueryString(queryString);
+
+ assertThat(parameters.get("name")).containsExactly("alice", "bob");
+ assertThat(parameters.get("age")).containsExactly("20", "30");
+ assertThat(parameters.get("page")).containsExactly("1");
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void parseEmptyQueryString(String queryString) {
+ QueryStringDecoder decoder = new QueryStringDecoder();
+
+ MultiValueMap parameters = decoder.parseQueryString(queryString);
+ assertThat(parameters).isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/webserver/http/SessionManagerTest.java b/src/test/java/webserver/http/SessionManagerTest.java
new file mode 100644
index 000000000..ce8b2d802
--- /dev/null
+++ b/src/test/java/webserver/http/SessionManagerTest.java
@@ -0,0 +1,31 @@
+package webserver.http;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+class SessionManagerTest {
+ @Test
+ void removeSession() {
+ String sessionId = getSessionId();
+ SessionManager sessionManager = SessionManager.getInstance();
+ sessionManager.createSession(sessionId);
+ sessionManager.removeSession(sessionId);
+
+ assertFalse(sessionManager.isSessionIdValid(sessionId));
+ }
+ @Test
+ void createSession(){
+ String sessionId = getSessionId();
+ SessionManager sessionManager = SessionManager.getInstance();
+ sessionManager.createSession(sessionId);
+
+ assertTrue(sessionManager.isSessionIdValid(sessionId));
+ }
+
+ String getSessionId() {
+ return UUID.randomUUID().toString();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/webserver/servlet/ServletMappingTest.java b/src/test/java/webserver/servlet/ServletMappingTest.java
new file mode 100644
index 000000000..5312aa579
--- /dev/null
+++ b/src/test/java/webserver/servlet/ServletMappingTest.java
@@ -0,0 +1,14 @@
+package webserver.servlet;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+class ServletMappingTest {
+ @Test
+ void givenPathMappedServlet(){
+ Servlet servlet = ServletMapping.match("/user/create");
+
+ assertEquals(servlet.getClass(), UserCreateServlet.class);
+ }
+}
\ No newline at end of file