Skip to content

Commit

Permalink
[알트] 1단계 - HTTP 웹 서버 구현 미션 제출합니다. (woowacourse#123)
Browse files Browse the repository at this point in the history
* docs: 기능목록 작성

* feat: Request를 읽어 Line 생성

* feat: path에 해당하는 html 파일을 읽어 응답

* feat: Request Parameter를 추출

* feat: RequestHeaders 추가

* feat: QueryParameters 추가

* feat: Uri 추가

* feat: GET 메서드로 회원가입하는 기능 구현

* RequestHeader 인스턴스 생성시 BufferedReader를 통해 직접 읽지 않고 IOUtils를 통해 읽어온 문자열 리스트를 받도록 변경

* refactor: http request 검증로직 추가

* refactor: POST 메서드로 회원가입 가능하도록 수정

* feat: 회원가입 완료 후 index.html 페이지로 이동하는 기능 구현

* feat: stylesheet 파일을 지원하도록 구현

* feat: 정적 리소스 식별을 위한 StaticResourceType 추가

* feat: 리소스 요청 처리를 위한 ResourceRequestHandler 추가

* feat: HttpResponse 추가

* RequestHandler에 집중된 응답 처리 책임을 위임하기 위한 조치

* refactor: 리소스 이외의 요청 처리를 위한 FrontController 추가

* BufferedReader를 통해 생성 가능하도록 변경
* 기타 Request 관련 리팩토링 수행
* 요청을 수행하는 Handler 추가
* 미리 지정된 요청을 찾기 위한 HandlerMapping 추가

* fix: Not Found 예외처리 코드 변경

* test: StaticResourceType 테스트케이스 추가

* 기존에 주석처리 되어있는 테스트 케이스 리팩토링

* feat: 처리되지 않은 예외에 대한 응답 형식 추가

* refactor: 문자열 일치 여부를 확인하는 일부 메서드명 변경

* matches -> equals~로 통일
* 문자열 패턴이 일치하는지 여부를 확인하는 메서드 matches -> anyMatch로 변경

* refactor: requestLine 변수명 수정

protocol -> version

* refactor: OS에 적합한 개행 문자를 사용하도록 변경

* refactor: 디미터 법칙 위반 사례 수정

* refactor: 불필요한 LinkedHashMap 사용 제거

* refactor: body가 없는 응답 객체 생성 방식 변경

* test: 프로젝트에서 사용되지 않는 테스트코드 제거

* refactor: Resource, Path에 따른 Handler Mapping방식 변경
  • Loading branch information
KS-KIM authored Sep 30, 2020
1 parent aad12e4 commit dcc0d78
Show file tree
Hide file tree
Showing 40 changed files with 1,016 additions and 319 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,25 @@
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 기능 목록

### 🚀 1단계 - HTTP 웹 서버 구현

- [X] http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다.
- [X] 모든 Request Header를 읽어온다.
- [X] Request Line에서 path를 분리한다.
- [X] path에 해당하는 파일 읽어 응답한다.
- [X] 회원가입 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다.
- [X] Request Parameter를 추출한다.
- [X] 사용자가 입력한 값을 파싱해 model.User 클래스에 저장한다.
- [X] 회원가입 기능이 정상적으로 동작하도록 구현한다.
- [X] http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한다.
- [X] Reuqest Body를 읽어온다.
- [X] User 객체를 생성한다.
- [X] 회원가입을 완료하면 /index.html 페이지로 이동한다.
- [X] 응답 헤더의 상태 코드로 302를 사용하여 리다이렉트 한다.
- [X] stylesheet 파일을 지원하도록 구현한다.
- [X] Stylesheet인 경우 응답 헤더의 Content-Type을 text/css로 전송한다.

## 우아한테크코스 코드리뷰
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
8 changes: 8 additions & 0 deletions src/main/java/http/HttpMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package http;

public enum HttpMethod {
GET,
POST,
PUT,
DELETE;
}
74 changes: 74 additions & 0 deletions src/main/java/http/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package http;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.List;
import java.util.Objects;

import utils.IOUtils;

public class HttpRequest {
private final RequestLine requestLine;
private final RequestHeaders requestHeaders;
private final RequestBody requestBody;

public HttpRequest(final RequestLine requestLine, final RequestHeaders requestHeaders,
final RequestBody requestBody) {
this.requestLine = Objects.requireNonNull(requestLine, "request line이 존재하지 않습니다.");
this.requestHeaders = Objects.requireNonNull(requestHeaders, "request headers가 존재하지 않습니다.");
this.requestBody = requestBody;
}

public static HttpRequest from(final BufferedReader bufferedReader) throws IOException {
List<String> requestLineAndHeader = IOUtils.readHeader(bufferedReader);
RequestLine line = RequestLine.from(requestLineAndHeader.get(0));
RequestHeaders headers = RequestHeaders.from(requestLineAndHeader.subList(1, requestLineAndHeader.size()));
if (headers.hasContentLength()) {
int contentLength = headers.getContentLength();
RequestBody body = RequestBody.from(IOUtils.readBody(bufferedReader, contentLength));
return new HttpRequest(line, headers, body);
}
return new HttpRequest(line, headers, null);
}

public boolean equalsMethod(final HttpMethod httpMethod) {
return requestLine.equalsMethod(httpMethod);
}

public boolean equalsPath(final String path) {
return requestLine.equalsPath(path);
}

public String getPath() {
return requestLine.getPath();
}

public Uri getUri() {
return requestLine.getUri();
}

public String getBodyValue(final String key) {
return requestBody.getValue(key);
}

public RequestLine getRequestLine() {
return requestLine;
}

public RequestHeaders getRequestHeaders() {
return requestHeaders;
}

public RequestBody getRequestBody() {
return requestBody;
}

@Override
public String toString() {
return "HttpRequest{" +
"requestLine=" + requestLine +
", requestHeaders=" + requestHeaders +
", requestBody=" + requestBody +
'}';
}
}
73 changes: 73 additions & 0 deletions src/main/java/http/HttpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package http;

import java.io.DataOutputStream;
import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpResponse {
private static final Logger logger = LoggerFactory.getLogger(HttpResponse.class);

private final DataOutputStream dataOutputStream;

public HttpResponse(final DataOutputStream dataOutputStream) {
this.dataOutputStream = dataOutputStream;
}

public void response200Header(String contentType, int lengthOfBodyContent) {
try {
dataOutputStream.writeBytes("HTTP/1.1 200 OK " + System.lineSeparator());
dataOutputStream.writeBytes("Content-Type: " + contentType + ";charset=utf-8" + System.lineSeparator());
dataOutputStream.writeBytes("Content-Length: " + lengthOfBodyContent + System.lineSeparator());
dataOutputStream.writeBytes(System.lineSeparator());
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public void response302Header(String location) {
try {
dataOutputStream.writeBytes("HTTP/1.1 302 Found " + System.lineSeparator());
dataOutputStream.writeBytes("Location: " + location + System.lineSeparator());
dataOutputStream.writeBytes(System.lineSeparator());
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public void response404Header() {
try {
dataOutputStream.writeBytes("HTTP/1.1 404 Not Found " + System.lineSeparator());
dataOutputStream.writeBytes(System.lineSeparator());
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public void response500Header() {
try {
dataOutputStream.writeBytes("HTTP/1.1 500 Internal Server Error " + System.lineSeparator());
dataOutputStream.writeBytes(System.lineSeparator());
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public void responseBody(byte[] body) {
try {
dataOutputStream.write(body, 0, body.length);
dataOutputStream.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}

public void emptyBody() {
try {
dataOutputStream.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
39 changes: 39 additions & 0 deletions src/main/java/http/QueryParameters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package http;

import static java.util.stream.Collectors.*;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;

public class QueryParameters {
private static final String QUERY_DELIMITER = "&";
private static final String QUERY_KEY_VALUE_DELIMITER = "=";

private final Map<String, String> parameters;

private QueryParameters(final Map<String, String> parameters) {
this.parameters = parameters;
}

public static QueryParameters from(final String queries) {
return Stream.of(queries.split(QUERY_DELIMITER))
.map(query -> query.split(QUERY_KEY_VALUE_DELIMITER))
.collect(collectingAndThen(toMap(query -> query[0], query -> query[1]), QueryParameters::new));
}

public String getParameter(final String key) {
return parameters.get(key);
}

public Map<String, String> getParameters() {
return Collections.unmodifiableMap(parameters);
}

@Override
public String toString() {
return "QueryParameters{" +
"parameters=" + parameters +
'}';
}
}
27 changes: 27 additions & 0 deletions src/main/java/http/RequestBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package http;

import static java.util.stream.Collectors.*;

import java.util.Map;
import java.util.stream.Stream;

public class RequestBody {
private static final String BODY_DELIMITER = "&";
private static final String BODY_KEY_VALUE_DELIMITER = "=";

private final Map<String, String> values;

public RequestBody(Map<String, String> values) {
this.values = values;
}

public static RequestBody from(final String values) {
return Stream.of(values.split(BODY_DELIMITER))
.map(value -> value.split(BODY_KEY_VALUE_DELIMITER))
.collect(collectingAndThen(toMap(value -> value[0], value -> value[1]), RequestBody::new));
}

public String getValue(final String key) {
return values.get(key);
}
}
40 changes: 40 additions & 0 deletions src/main/java/http/RequestHeaders.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package http;

import static java.util.stream.Collectors.*;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class RequestHeaders {
private static final String HEADER_KEY_VALUE_DELIMITER = ": ";

private final Map<String, String> headers;

private RequestHeaders(Map<String, String> headers) {
this.headers = Objects.requireNonNull(headers, "request headers가 존재하지 않습니다.");
}

public static RequestHeaders from(final List<String> headers) {
return headers.stream()
.map(header -> header.split(HEADER_KEY_VALUE_DELIMITER))
.collect(collectingAndThen(toMap(header -> header[0], header -> header[1]), RequestHeaders::new));
}

public boolean hasContentLength() {
return headers.containsKey("Content-Length");
}

public int getContentLength() {
return Integer.parseInt(getHeader("Content-Length"));
}

public String getHeader(final String key) {
return headers.get(key);
}

public Map<String, String> getHeaders() {
return Collections.unmodifiableMap(headers);
}
}
59 changes: 59 additions & 0 deletions src/main/java/http/RequestLine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package http;

import java.util.Objects;

public class RequestLine {
private static final String REQUEST_LINE_DELIMITER = " ";
private static final int REQUEST_LINE_TOTAL_PARTS_COUNT = 3;

private final HttpMethod method;
private final Uri uri;
private final String version;

private RequestLine(final HttpMethod method, final Uri uri, final String version) {
this.method = Objects.requireNonNull(method, "http 메서드가 존재하지 않습니다.");
this.uri = Objects.requireNonNull(uri, "uri가 존재하지 않습니다.");
this.version = Objects.requireNonNull(version, "protocol이 존재하지 않습니다.");
}

public static RequestLine from(final String requestLine) {
String[] splitLine = requestLine.split(REQUEST_LINE_DELIMITER);
if (splitLine.length != REQUEST_LINE_TOTAL_PARTS_COUNT) {
throw new IllegalArgumentException("request line 형식이 올바르지 않습니다. request line count: " + splitLine.length);
}
return new RequestLine(HttpMethod.valueOf(splitLine[0]), Uri.from(splitLine[1]), splitLine[2]);
}

public boolean equalsMethod(final HttpMethod httpMethod) {
return method.equals(httpMethod);
}

public boolean equalsPath(final String path) {
return uri.equalsPath(path);
}

public String getPath() {
return uri.getPath();
}

public HttpMethod getMethod() {
return method;
}

public Uri getUri() {
return uri;
}

public String getVersion() {
return version;
}

@Override
public String toString() {
return "RequestLine{" +
"method=" + method +
", uri='" + uri + '\'' +
", protocol='" + version + '\'' +
'}';
}
}
Loading

0 comments on commit dcc0d78

Please sign in to comment.