From fd1aed0098b4d42ccdded24f65bf1d6f85ccf49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=98=90=EB=A7=81?= Date: Sat, 12 Sep 2020 22:13:08 +0900 Subject: [PATCH] =?UTF-8?q?[=EB=98=90=EB=A7=81]=201=EB=8B=A8=EA=B3=84=20-?= =?UTF-8?q?=20HTTP=20=EC=9B=B9=20=EC=84=9C=EB=B2=84=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs : 요구사항1의 구현 기능 목록 작성 * feat : 모든 Request Header 출력하기 * feat : Request에서 path 분리하기 * feat : path에 해당하는 파일 읽어 응답하기 * docs : 요구사항2의 구현 기능 목록 작성 * feat : Request Parameter 추출 및 User 객체 생성 * docs : 요구사항3의 구현 기능 목록 작성 * feat : form.html 파일의 form 태그 method를 get에서 post로 수정 * refactor : HttpRequest를 RequestHeader와 RequestBody로 분리 * refactor : HttpRequestParser 유틸 클래스 생성 * docs : 요구사항4의 구현 기능 목록 작성 * feat : 요청에 따라 다른 HttpResponse를 내려준다. * docs : 요구사항5의 구현 기능 목록 작성 * refactor : RequestHeader 값의 자료형을 List에서 Map으로 변경 * refactor : HeaderProperty 생성 * feat : 응답에 따라 Content-Type을 변경하여 Stylesheet 파일을 지원하도록 구현 * feat : status code를 302로 변경한 후, Location 값에 리다이렉션 할 페이지를 넣어 응답 * refactor : 변수 정리 및 파일 끝 개행 추가 --- README.md | 45 +++++++-- src/main/java/db/DataBase.java | 6 +- src/main/java/model/User.java | 3 +- src/main/java/utils/FileIoUtils.java | 25 ++++- src/main/java/utils/HttpRequestParser.java | 36 +++++++ src/main/java/web/HeaderProperty.java | 19 ++++ src/main/java/web/HttpRequest.java | 45 +++++++++ src/main/java/web/HttpResponse.java | 95 +++++++++++++++++++ src/main/java/web/Method.java | 12 +++ src/main/java/web/RequestBody.java | 51 ++++++++++ src/main/java/web/RequestHeader.java | 71 ++++++++++++++ src/main/java/web/RequestUri.java | 84 ++++++++++++++++ src/main/java/web/ResourceMatcher.java | 39 ++++++++ src/main/java/webserver/RequestHandler.java | 40 +++----- src/main/resources/templates/user/form.html | 2 +- .../java/coordinate/FigureFactoryTest.java | 4 +- src/test/java/utils/HandlebarsTest.java | 7 +- .../java/utils/HttpRequestParserTest.java | 47 +++++++++ src/test/java/utils/IOUtilsTest.java | 6 +- src/test/java/web/HttpRequestFixture.java | 45 +++++++++ src/test/java/web/HttpRequestTest.java | 31 ++++++ src/test/java/web/RequestBodyTest.java | 26 +++++ src/test/java/web/RequestHeaderTest.java | 56 +++++++++++ src/test/java/web/RequestUriTest.java | 54 +++++++++++ src/test/java/web/ResourceMatcherTest.java | 17 ++++ src/test/java/webserver/ExecutorsTest.java | 8 +- 26 files changed, 822 insertions(+), 52 deletions(-) create mode 100644 src/main/java/utils/HttpRequestParser.java create mode 100644 src/main/java/web/HeaderProperty.java create mode 100644 src/main/java/web/HttpRequest.java create mode 100644 src/main/java/web/HttpResponse.java create mode 100644 src/main/java/web/Method.java create mode 100644 src/main/java/web/RequestBody.java create mode 100644 src/main/java/web/RequestHeader.java create mode 100644 src/main/java/web/RequestUri.java create mode 100644 src/main/java/web/ResourceMatcher.java create mode 100644 src/test/java/utils/HttpRequestParserTest.java create mode 100644 src/test/java/web/HttpRequestFixture.java create mode 100644 src/test/java/web/HttpRequestTest.java create mode 100644 src/test/java/web/RequestBodyTest.java create mode 100644 src/test/java/web/RequestHeaderTest.java create mode 100644 src/test/java/web/RequestUriTest.java create mode 100644 src/test/java/web/ResourceMatcherTest.java diff --git a/README.md b/README.md index 95ec3e4fd..4f2c22e65 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,38 @@ # 웹 애플리케이션 서버 -## 진행 방법 -* 웹 애플리케이션 서버 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. - -## 우아한테크코스 코드리뷰 -* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file +## 1단계 - HTTP 웹 서버 구현 +### 요구사항1 +> http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다. + +*구현 기능 목록* +- [x] Request Header를 파싱하여 원하는 정보 찾기 + - [x] 모든 Request header 출력하기 + - [x] Request에서 path 분리하기 +- [x] path에 해당하는 파일 읽어 응답하기 + +### 요구사항2 +> “회원가입” 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다. + +*구현 기능 목록* +- [x] Request Parameter 추출 +- [x] 사용자가 입력한 값 저장 + +### 요구사항3 +> http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다. + +*구현 기능 목록* +- [x] form.html 파일의 form 태그 method를 get에서 post로 수정 +- [x] Request Body의 값 추출하기 + +### 요구사항4 +> “회원가입”을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. +> 따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다.즉, 브라우저의 URL이 /index.html로 변경해야 한다. + +*구현 기능 목록* +- [x] 요청에 따라 다른 HttpResponse를 응답 + - [x] status code를 302로 변경한 후, Location 값에 리다이렉션 할 페이지를 넣어 응답 + + ### 요구사항5 + > 지금까지 구현한 소스 코드는 stylesheet 파일을 지원하지 못하고 있다. Stylesheet 파일을 지원하도록 구현하도록 한다. + + *구현 기능 목록* +- [x] 응답에 따라 Content-Type을 변경하여 Stylesheet 파일을 지원하도록 구현 \ No newline at end of file diff --git a/src/main/java/db/DataBase.java b/src/main/java/db/DataBase.java index b9419a4a3..b4e2626ea 100644 --- a/src/main/java/db/DataBase.java +++ b/src/main/java/db/DataBase.java @@ -3,15 +3,19 @@ import java.util.Collection; import java.util.Map; -import com.google.common.collect.Maps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.common.collect.Maps; import model.User; public class DataBase { + private static final Logger logger = LoggerFactory.getLogger(DataBase.class); private static Map users = Maps.newHashMap(); public static void addUser(User user) { users.put(user.getUserId(), user); + logger.debug("USER 회원가입 성공 : " + user.toString()); } public static User findUserById(String userId) { diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java index b7abb7304..1a5e5c73e 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/model/User.java @@ -31,6 +31,7 @@ public String getEmail() { @Override public String toString() { - return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]"; + return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + + email + "]"; } } diff --git a/src/main/java/utils/FileIoUtils.java b/src/main/java/utils/FileIoUtils.java index ab027f93f..3dc2d3958 100644 --- a/src/main/java/utils/FileIoUtils.java +++ b/src/main/java/utils/FileIoUtils.java @@ -2,13 +2,32 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class FileIoUtils { - public static byte[] loadFileFromClasspath(String filePath) throws IOException, URISyntaxException { - Path path = Paths.get(FileIoUtils.class.getClassLoader().getResource(filePath).toURI()); - return Files.readAllBytes(path); + private static final Logger logger = LoggerFactory.getLogger(FileIoUtils.class); + + public static byte[] loadFileFromClasspath(String filePath) throws IOException { + URL resource = FileIoUtils.class.getClassLoader() + .getResource(filePath); + try { + if (resource != null) { + Path path = Paths.get(resource.toURI()); + return Files.readAllBytes(path); + } + return null; + } catch (URISyntaxException e) { + logger.error(e.getMessage()); + throw new IllegalArgumentException(String.format( + "[%s] is not formatted strictly according to RFC2396 and cannot be converted to a URI", + filePath)); + } } } + diff --git a/src/main/java/utils/HttpRequestParser.java b/src/main/java/utils/HttpRequestParser.java new file mode 100644 index 000000000..7b8e391e6 --- /dev/null +++ b/src/main/java/utils/HttpRequestParser.java @@ -0,0 +1,36 @@ +package utils; + +import static web.HeaderProperty.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequestParser { + public static final String EMPTY = ""; + public static final String HEADER_DATA_DELIMITER = ": "; + public static final int KEY_INDEX = 0; + public static final int VALUE_INDEX = 1; + private static final String KEY_VALUE_DELIMITER = "="; + + public static Map parsingRequestHeader(BufferedReader br) throws IOException { + Map request = new HashMap<>(); + request.put(REQUEST_LINE.getName(), br.readLine()); + String line = br.readLine(); + while (line != null && !EMPTY.equals(line)) { + String[] headerData = line.split(HEADER_DATA_DELIMITER); + request.put(headerData[KEY_INDEX], headerData[VALUE_INDEX]); + line = br.readLine(); + } + return request; + } + + public static Map parsingData(String s) { + return Arrays.stream(s.split("&")) + .collect(Collectors.toMap(param -> param.split(KEY_VALUE_DELIMITER)[0], + param -> param.split(KEY_VALUE_DELIMITER)[1])); + } +} diff --git a/src/main/java/web/HeaderProperty.java b/src/main/java/web/HeaderProperty.java new file mode 100644 index 000000000..57cbeebfe --- /dev/null +++ b/src/main/java/web/HeaderProperty.java @@ -0,0 +1,19 @@ +package web; + +public enum HeaderProperty { + REQUEST_LINE("requestLine"), + HOST("Host"), + CONNECTION("Connection"), + CONTENT_LENGTH("Content-Length"), + ACCEPT("Accept"); + + private String name; + + HeaderProperty(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/web/HttpRequest.java b/src/main/java/web/HttpRequest.java new file mode 100644 index 000000000..02892f7a4 --- /dev/null +++ b/src/main/java/web/HttpRequest.java @@ -0,0 +1,45 @@ +package web; + +import java.io.BufferedReader; +import java.io.IOException; + +import utils.IOUtils; + +public class HttpRequest { + private RequestHeader requestHeader; + private RequestBody requestBody; + + public HttpRequest(BufferedReader br) throws IOException { + this.requestHeader = new RequestHeader(br); + if (requestHeader.isPost()) { + String data = IOUtils.readData(br, requestHeader.getContentLength()); + this.requestBody = new RequestBody(data); + } else { + this.requestBody = null; + } + } + + public boolean isPost() { + return requestHeader.isPost(); + } + + public boolean isStaticFile() { + return requestHeader.isStaticFile(); + } + + public RequestUri getRequestUri() { + return requestHeader.getRequestUri(); + } + + public RequestBody getRequestBody() { + return requestBody; + } + + @Override + public String toString() { + return "HttpRequest{" + + "requestHeader=" + requestHeader + + ", requestBody=" + requestBody + + '}'; + } +} diff --git a/src/main/java/web/HttpResponse.java b/src/main/java/web/HttpResponse.java new file mode 100644 index 000000000..4f9420264 --- /dev/null +++ b/src/main/java/web/HttpResponse.java @@ -0,0 +1,95 @@ +package web; + +import static web.RequestUri.*; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import db.DataBase; +import model.User; +import utils.FileIoUtils; + +public class HttpResponse { + public static final String NEW_LINE = System.lineSeparator(); + + private static final Logger logger = LoggerFactory.getLogger(HttpResponse.class); + + private DataOutputStream dos; + + public HttpResponse(DataOutputStream dos) { + this.dos = dos; + } + + public void process(HttpRequest httpRequest) { + if (httpRequest.isStaticFile()) { + processFile(httpRequest); + } else { + processApi(httpRequest); + } + } + + private void processFile(HttpRequest httpRequest) { + RequestUri requestUri = httpRequest.getRequestUri(); + String resourcePath = requestUri.findPath() + requestUri.getUri(); + byte[] content = null; + try { + content = FileIoUtils.loadFileFromClasspath( + resourcePath); + } catch (IOException e) { + logger.error(e.getMessage()); + } + if (content != null) { + String response = response200Header(requestUri.findContentType(), content); + toDataOutputStream(response); + responseBody(content); + } + } + + private void processApi(HttpRequest httpRequest) { + if (httpRequest.isPost()) { + Map body = httpRequest.getRequestBody().getFormData(); + User user = new User(body.get("userId"), body.get("password"), body.get("name"), + body.get("email")); + DataBase.addUser(user); + String response = response302Header("http://localhost:8080" + INDEX_HTML); + toDataOutputStream(response); + responseBody(new byte[0]); + } + } + + private String response200Header(String contentType, byte[] content) { + return "HTTP/1.1 200 OK" + NEW_LINE + + "Content-Type: " + contentType + NEW_LINE + + "Content-Length: " + content.length + NEW_LINE + + NEW_LINE; + } + + private String response302Header(String redirectUrl) { + return "HTTP/1.1 302 Found" + NEW_LINE + + "Location: " + redirectUrl + NEW_LINE + + NEW_LINE; + } + + private void toDataOutputStream(String response) { + try { + dos.writeBytes(response); + logger.debug(response); + } catch (IOException e) { + logger.error(e.getMessage()); + e.printStackTrace(); + } + } + + private void responseBody(byte[] content) { + try { + dos.write(content, 0, content.length); + dos.flush(); + } catch (IOException e) { + logger.error(e.getMessage()); + } + } +} diff --git a/src/main/java/web/Method.java b/src/main/java/web/Method.java new file mode 100644 index 000000000..7c682c44a --- /dev/null +++ b/src/main/java/web/Method.java @@ -0,0 +1,12 @@ +package web; + +public enum Method { + OPTIONS, + HEAD, + POST, + GET, + PUT, + DELETE, + TRACE, + CONNECT; +} diff --git a/src/main/java/web/RequestBody.java b/src/main/java/web/RequestBody.java new file mode 100644 index 000000000..4d930d4e8 --- /dev/null +++ b/src/main/java/web/RequestBody.java @@ -0,0 +1,51 @@ +package web; + +import static utils.HttpRequestParser.*; +import static web.HttpResponse.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +public class RequestBody { + Map requestBody; + + public RequestBody(String data) { + this.requestBody = parsingData(data); + } + + public RequestBody(BufferedReader br) throws IOException { + String line = br.readLine(); + while (line == null || line.isEmpty() || NEW_LINE.equals(line)) { + line = br.readLine(); + } + this.requestBody = parsingData(line); + } + + public Map getFormData() { + return requestBody; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RequestBody that = (RequestBody)o; + return Objects.equals(requestBody, that.requestBody); + } + + @Override + public int hashCode() { + return Objects.hash(requestBody); + } + + @Override + public String toString() { + return "RequestBody{" + + "requestBody=" + requestBody + + '}'; + } +} diff --git a/src/main/java/web/RequestHeader.java b/src/main/java/web/RequestHeader.java new file mode 100644 index 000000000..3a10e445c --- /dev/null +++ b/src/main/java/web/RequestHeader.java @@ -0,0 +1,71 @@ +package web; + +import static utils.HttpRequestParser.*; +import static web.HeaderProperty.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +public class RequestHeader { + private static final String BLANK = " "; + private static final int METHOD_INDEX = 0; + private static final int PATH_INDEX = 1; + + private final Map header; + private Method method; + private RequestUri requestUri; + + public RequestHeader(BufferedReader br) throws IOException { + this.header = parsingRequestHeader(br); + this.method = Method.valueOf(getRequestLine()[METHOD_INDEX]); + this.requestUri = new RequestUri(getRequestLine()[PATH_INDEX]); + } + + public boolean isStaticFile() { + return requestUri.isStaticFile(); + } + + public boolean isPost() { + return Method.POST.equals(method); + } + + public int getContentLength() { + return Integer.parseInt(header.get(CONTENT_LENGTH.getName())); + } + + public RequestUri getRequestUri() { + return requestUri; + } + + private String[] getRequestLine() { + return header.get(REQUEST_LINE.getName()).split(BLANK); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RequestHeader that = (RequestHeader)o; + return method == that.method && + Objects.equals(requestUri, that.requestUri) && + Objects.equals(header, that.header); + } + + @Override + public int hashCode() { + return Objects.hash(method, requestUri, header); + } + + @Override + public String toString() { + return "RequestHeader{" + + "method=" + method + + ", requestUri=" + requestUri + + ", header=" + header + + '}'; + } +} diff --git a/src/main/java/web/RequestUri.java b/src/main/java/web/RequestUri.java new file mode 100644 index 000000000..b90866c4c --- /dev/null +++ b/src/main/java/web/RequestUri.java @@ -0,0 +1,84 @@ +package web; + +import static utils.HttpRequestParser.*; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class RequestUri { + public static final int URI_INDEX = 0; + public static final String PARAMETER_DELIMITER = "?"; + static final String INDEX_HTML = "/index.html"; + private static final int PARAMETER_INDEX = 1; + + private String uri; + private Map parameters; + + public RequestUri(String uri) { + this.uri = uri; + this.parameters = parsingParameters(); + } + + public boolean isStaticFile() { + return findResource().isPresent(); + } + + public Optional findResource() { + return ResourceMatcher.fromUri(uri); + } + + public String findPath() { + if (findResource().isPresent()) { + return findResource().get().getResourcePath(); + } + return EMPTY; + } + + public String findContentType() { + if (findResource().isPresent()) { + return findResource().get().getContentType(); + } + return "*/*"; + } + + private Map parsingParameters() { + if (!uri.contains(PARAMETER_DELIMITER)) { + return null; + } + String[] requestUri = uri.split("\\" + PARAMETER_DELIMITER); + uri = requestUri[URI_INDEX]; + return parsingData(requestUri[PARAMETER_INDEX]); + } + + public Map getParameters() { + return parameters; + } + + public String getUri() { + return uri; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RequestUri that = (RequestUri)o; + return Objects.equals(uri, that.uri); + } + + @Override + public int hashCode() { + return Objects.hash(uri); + } + + @Override + public String toString() { + return "RequestUri{" + + "uri='" + uri + '\'' + + ", parameters=" + parameters + + '}'; + } +} diff --git a/src/main/java/web/ResourceMatcher.java b/src/main/java/web/ResourceMatcher.java new file mode 100644 index 000000000..73a6c788c --- /dev/null +++ b/src/main/java/web/ResourceMatcher.java @@ -0,0 +1,39 @@ +package web; + +import java.util.Arrays; +import java.util.Optional; + +public enum ResourceMatcher { + HTML(".html", "./templates", "text/html;charset=UTF-8"), + ICO(".ico", "./templates", "*/*"), + CSS(".css", "./static", "text/css;charset=UTF-8"), + JS(".js", "./static", "application/javascript;charset=UTF-8"), + AVG(".avg", "./static", "*/*"), + TTF(".ttf", "./static", "*/*"), + WOFF(".woff", "./static", "*/*"), + WOFF2(".woff2", "./static", "*/*"); + + private final String extension; + private final String pathPrefix; + private final String contentType; + + ResourceMatcher(String extension, String pathPrefix, String contentType) { + this.extension = extension; + this.pathPrefix = pathPrefix; + this.contentType = contentType; + } + + public static Optional fromUri(String uri) { + return Arrays.stream(values()) + .filter(value -> uri.endsWith(value.extension)) + .findFirst(); + } + + public String getResourcePath() { + return pathPrefix; + } + + public String getContentType() { + return contentType; + } +} \ No newline at end of file diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index c7c0134ae..cc73f82b9 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -1,14 +1,20 @@ package webserver; +import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import web.HttpRequest; +import web.HttpResponse; + public class RequestHandler implements Runnable { private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); @@ -19,35 +25,17 @@ public RequestHandler(Socket connectionSocket) { } public void run() { - logger.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), + logger.debug("New Client Connect! Connected IP : {}, Port : {}", + connection.getInetAddress(), connection.getPort()); - try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { - // TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다. + BufferedReader br = new BufferedReader( + new InputStreamReader(in, StandardCharsets.UTF_8)); 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(); + HttpRequest httpRequest = new HttpRequest(br); + logger.debug(httpRequest.toString()); + HttpResponse httpResponse = new HttpResponse(dos); + httpResponse.process(httpRequest); } catch (IOException e) { logger.error(e.getMessage()); } diff --git a/src/main/resources/templates/user/form.html b/src/main/resources/templates/user/form.html index 96fe1bd3a..f7a3b5612 100644 --- a/src/main/resources/templates/user/form.html +++ b/src/main/resources/templates/user/form.html @@ -75,7 +75,7 @@
-
+
diff --git a/src/test/java/coordinate/FigureFactoryTest.java b/src/test/java/coordinate/FigureFactoryTest.java index 91d1a8ff4..ce5b19f00 100644 --- a/src/test/java/coordinate/FigureFactoryTest.java +++ b/src/test/java/coordinate/FigureFactoryTest.java @@ -1,11 +1,11 @@ package coordinate; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; public class FigureFactoryTest { @Test diff --git a/src/test/java/utils/HandlebarsTest.java b/src/test/java/utils/HandlebarsTest.java index 040de9cf4..cb205469e 100644 --- a/src/test/java/utils/HandlebarsTest.java +++ b/src/test/java/utils/HandlebarsTest.java @@ -1,13 +1,14 @@ package utils; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + 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 model.User; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class HandlebarsTest { private static final Logger log = LoggerFactory.getLogger(HandlebarsTest.class); diff --git a/src/test/java/utils/HttpRequestParserTest.java b/src/test/java/utils/HttpRequestParserTest.java new file mode 100644 index 000000000..893fe6f7f --- /dev/null +++ b/src/test/java/utils/HttpRequestParserTest.java @@ -0,0 +1,47 @@ +package utils; + +import static org.junit.jupiter.api.Assertions.*; +import static web.HttpRequestFixture.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import web.HttpRequestFixture; + +class HttpRequestParserTest { + @DisplayName("요청에서 header를 추출한다.") + @Test + void parsingRequestHeader() throws IOException { + Map expected = new HashMap() {{ + put("requestLine", "POST /user/create HTTP/1.1"); + put("Host", "localhost:8080"); + put("Connection", "keep-alive"); + put("Content-Length", String.valueOf(JAVAJIGI_DATA.length())); + put("Accept", "*/*"); + }}; + BufferedReader br = HttpRequestFixture.createBufferedReader( + HttpRequestFixture.REQUEST); + + Map actual = HttpRequestParser.parsingRequestHeader(br); + + assertEquals(expected, actual); + } + + @DisplayName("parameter와 body의 데이터를 Map으로 추출한다.") + @Test + void parsingData() { + Map expected = new HashMap() {{ + put("userId", "javajigi"); + put("password", "password"); + put("name", "%EB%B0%95%EC%9E%AC%EC%84%B1"); + put("email", "javajigi%40slipp.net"); + }}; + Map actual = HttpRequestParser.parsingData(JAVAJIGI_DATA); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/utils/IOUtilsTest.java b/src/test/java/utils/IOUtilsTest.java index 20ef0c3e7..6c2a1fa4a 100644 --- a/src/test/java/utils/IOUtilsTest.java +++ b/src/test/java/utils/IOUtilsTest.java @@ -1,12 +1,12 @@ package utils; +import java.io.BufferedReader; +import java.io.StringReader; + import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; -import java.io.StringReader; - public class IOUtilsTest { private static final Logger logger = LoggerFactory.getLogger(IOUtilsTest.class); diff --git a/src/test/java/web/HttpRequestFixture.java b/src/test/java/web/HttpRequestFixture.java new file mode 100644 index 000000000..0971eee0d --- /dev/null +++ b/src/test/java/web/HttpRequestFixture.java @@ -0,0 +1,45 @@ +package web; + +import static utils.HttpRequestParser.*; +import static web.HttpResponse.*; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +public class HttpRequestFixture { + public static final String JAVAJIGI_DATA = "userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net"; + public static final String HEADER = "POST /user/create HTTP/1.1" + NEW_LINE + + "Host: localhost:8080" + NEW_LINE + + "Connection: keep-alive" + NEW_LINE + + "Content-Length: " + JAVAJIGI_DATA.length() + NEW_LINE + + "Accept: */*" + NEW_LINE + + EMPTY; + static final String GET = "GET"; + static final String POST = "POST"; + static final String ROOT = "/"; + static final String INDEX_HTML = "/index.html"; + static final String BODY = NEW_LINE + + JAVAJIGI_DATA + + NEW_LINE + + EMPTY; + + public static final String REQUEST = HEADER + BODY; + + public static HttpRequest createRequest(String method, String uri) throws IOException { + String request = method + " " + uri + " HTTP/1.1" + NEW_LINE + + "Host: localhost:8080" + NEW_LINE + + "Connection: keep-alive" + NEW_LINE + + "Content-Length: " + JAVAJIGI_DATA.length() + NEW_LINE + + "Accept: */*" + NEW_LINE + + EMPTY; + return new HttpRequest(createBufferedReader(request)); + } + + public static BufferedReader createBufferedReader(String input) { + return new BufferedReader( + new InputStreamReader( + new ByteArrayInputStream(input.getBytes()))); + } +} diff --git a/src/test/java/web/HttpRequestTest.java b/src/test/java/web/HttpRequestTest.java new file mode 100644 index 000000000..a5cd9e58b --- /dev/null +++ b/src/test/java/web/HttpRequestTest.java @@ -0,0 +1,31 @@ +package web; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static web.HttpRequestFixture.*; + +import java.io.IOException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HttpRequestTest { + @DisplayName("HttpRequest를 생성한다.") + @Test + public void HttpRequest() throws IOException { + assertThat(createRequest(GET, INDEX_HTML)) + .isInstanceOf(HttpRequest.class); + } + + @DisplayName("요청이 POST인지 확인한다.") + @Test + void isPost() throws IOException { + assertTrue(new HttpRequest(createBufferedReader(REQUEST)).isPost()); + } + + @DisplayName("요청의 Uri를 추출한다.") + @Test + void getRequestUri() throws IOException { + assertEquals(INDEX_HTML, createRequest(GET, INDEX_HTML).getRequestUri().getUri()); + } +} diff --git a/src/test/java/web/RequestBodyTest.java b/src/test/java/web/RequestBodyTest.java new file mode 100644 index 000000000..595b47524 --- /dev/null +++ b/src/test/java/web/RequestBodyTest.java @@ -0,0 +1,26 @@ +package web; + +import static org.junit.jupiter.api.Assertions.*; +import static web.HttpRequestFixture.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RequestBodyTest { + @DisplayName("body의 데이터를 Map으로 추출한다.") + @Test + void getFormData() throws IOException { + Map expected = new HashMap() {{ + put("userId", "javajigi"); + put("password", "password"); + put("name", "%EB%B0%95%EC%9E%AC%EC%84%B1"); + put("email", "javajigi%40slipp.net"); + }}; + Map actual = new RequestBody(createBufferedReader(BODY)).getFormData(); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/web/RequestHeaderTest.java b/src/test/java/web/RequestHeaderTest.java new file mode 100644 index 000000000..7e3da28bd --- /dev/null +++ b/src/test/java/web/RequestHeaderTest.java @@ -0,0 +1,56 @@ +package web; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static utils.HttpRequestParser.*; +import static web.HttpRequestFixture.*; +import static web.HttpResponse.*; +import static web.RequestHeader.*; + +import java.io.BufferedReader; +import java.io.IOException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RequestHeaderTest { + static RequestHeader createHeader(String method, String uri) throws IOException { + String request = method + " " + uri + " HTTP/1.1" + NEW_LINE + + "Host: localhost:8080" + NEW_LINE + + "Connection: keep-alive" + NEW_LINE + + "Content-Length: " + JAVAJIGI_DATA.length() + NEW_LINE + + "Accept: */*" + NEW_LINE + + EMPTY; + BufferedReader br = createBufferedReader(request); + return new RequestHeader(br); + } + + @DisplayName("RequestHeader를 생성한다.") + @Test + public void RequestHeader() throws IOException { + assertThat(createHeader(GET, INDEX_HTML)) + .isInstanceOf(RequestHeader.class); + } + + @DisplayName("정적 파일에 대한 요청인지 확인한다.") + @Test + public void isStaticFile() throws IOException { + assertTrue(createHeader(GET, INDEX_HTML).isStaticFile()); + } + + @DisplayName("POST 요청인지 확인한다.") + @Test + public void isPost() throws IOException { + assertTrue(createHeader(POST, ROOT).isPost()); + } + + @DisplayName("요청의 Uri를 추출한다.") + @Test + public void getRequestUri() throws IOException { + RequestUri expected = new RequestUri(INDEX_HTML); + + RequestUri actual = createHeader(GET, INDEX_HTML).getRequestUri(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/web/RequestUriTest.java b/src/test/java/web/RequestUriTest.java new file mode 100644 index 000000000..be59a3524 --- /dev/null +++ b/src/test/java/web/RequestUriTest.java @@ -0,0 +1,54 @@ +package web; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RequestUriTest { + + @DisplayName("uri가 정적 파일인지 확인한다.") + @Test + void isStaticFile() { + RequestUri uri = new RequestUri("user/create/form.html"); + assertTrue(uri.isStaticFile()); + } + + @DisplayName("요청 자원의 경로를 확인한다.") + @Test + public void findPath() { + String expected = ResourceMatcher.JS.getResourcePath(); + String actual = new RequestUri("js/script.js").findPath(); + assertEquals(expected, actual); + } + + @DisplayName("요청 자원의 contentType을 확인한다.") + @Test + public void getContentType() { + String expected = ResourceMatcher.JS.getContentType(); + String actual = new RequestUri("js/script.js").findContentType(); + assertEquals(expected, actual); + } + + @DisplayName("uri에 담긴 파라미터를 추출한다.") + @Test + void getParameter() { + Map expected = new HashMap() { + { + put("userId", "javajigi"); + put("password", "password"); + put("name", "%EB%B0%95%EC%9E%AC%EC%84%B1"); + put("email", "javajigi%40slipp.net"); + } + }; + RequestUri uri = new RequestUri( + "/create?userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net"); + + Map actual = uri.getParameters(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/web/ResourceMatcherTest.java b/src/test/java/web/ResourceMatcherTest.java new file mode 100644 index 000000000..2d8e3b404 --- /dev/null +++ b/src/test/java/web/ResourceMatcherTest.java @@ -0,0 +1,17 @@ +package web; + +import static org.junit.jupiter.api.Assertions.*; +import static web.HttpRequestFixture.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResourceMatcherTest { + + @DisplayName("uri에 해당하는 Resource를 찾아준다.") + @Test + void fromUri() { + ResourceMatcher actual = ResourceMatcher.fromUri(INDEX_HTML).get(); + assertEquals(ResourceMatcher.HTML, actual); + } +} diff --git a/src/test/java/webserver/ExecutorsTest.java b/src/test/java/webserver/ExecutorsTest.java index ab28c7422..b39c39295 100644 --- a/src/test/java/webserver/ExecutorsTest.java +++ b/src/test/java/webserver/ExecutorsTest.java @@ -1,14 +1,14 @@ package webserver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.StopWatch; - import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StopWatch; + public class ExecutorsTest { private static final Logger logger = LoggerFactory.getLogger(ExecutorsTest.class);