Skip to content

Commit e5a610c

Browse files
authored
[톰캣 구현하기 - 1, 2단계] 푸우(백승준) 미션 제출합니다. (#311)
* test: IOStreamTest 작성 * test: FileTest 작성 * docs: 기능 목록 작성 * feat: index.html 파일 반환 구현 * feat: 정적 파일을 반환하는 ResourceProvider 생성 * feat: CSS 파일 반환 구현 * feat: Method 구현 * feat: RequestLine 객체화 * fix: 디렉터리 조회시 파일을 반환하지 않도록 변경 * feat: Http11Processor 가 RequestLine 사용하도록 변경 * fix: ? 를 contatins 과 split 사용에 따라 분리한다 * style: 패키지 변경 * feat: 컨트롤러 정의 * feat: 1. 정적파일을 찾은 후 2. 컨트롤러를 찾고 3. 못 찾았다면 Not Found page 반환 * fix: queryParam 이 없더라도 login page 를 반환하도록 변경 * docs: 추가 기능 목록 추가 * feat: Response 객체 생성 * feat: Redirect 기능 구현 * feat: Request 객체 구현 * feat: 회원가입 구현 및 로그인 post 변경 * feat: Cookie 객체 생성 * feat: 로그인 혹은 회원가입 시 cookie 발급 * feat: Session, SessionManager 생성 * feat: LoginService 가 유저를 세션에 저장한다 * feat: 로그인 되어 Session 에 저장된 유저라면 login, register 요청은 index 로 리다이렉션 한다. * style: 쿠키 객체 이름 변경 * refactor: 파일을 Files 를 이용해 읽어온다. * refactor: 로그인 검증을 SessionManager 가 담당하도록 변경 * fix: 세션 쿠키가 없을 경우 empty 를 반환한다. * refactor: sonarLint 반영 * refactor: sonarLint 반영 * refactor: Session 객체를 상속 받지 않도록 변경 * style: Request, Response, HttpResponse, Method-> HttpRequest, HttpResponse, HttpStatusCode HttpMethod 변경 * refactor: processor 에서 null 대신 예외를 반환하도록 변경 * refactor: HttpRequest 생성을 HttpRequest 에게 위임 * style: processor 메서드 순서 변경 * feat: RequestHeaders 필드 private 화 * refactor: HttpRequest 의 불필요한 메서드 지역 변수 제거 및 null 대신 예외를 발생시키도록 변경 * refactor: HttpResponse 가 기본 상태코드를 가지지 않도록 변경 및 인스턴스 변수의 final 화 * refactor: Cookie 변환 로직을 Cookies 에게 위임 * fix: headers 재활용을 위해 메서드 지역 변수 생성 * refactor: RequestHeaders 가 Cookies 를 가지고 있도록 변경 * refactor: Session 생성 책임을 SessionManger 에게 할당 * refactor: 정적 파일 response 반환 책임을 ResourceProvider 에게 위임, 컨트롤러 response 반환 책임을 HandlerMapper 에게 위임 * style: HandlerMapper 오타 수정 * style: handler 반환 메서드 private * fix: 잘못 수정된 html method 수정 * refactor: 잘못된 Class 명명 수정 및 Body 값에 대한 Optional 처리 * refactor: toURI -> getFile 을 통한 exception handling 과정 삭제 * refactor: css 반환 헤더에 utf-8 적용 및 text/plain header 추가 * refactor: ResourceProvider 에서 존재하지 않는 파일을 읽는다면 null 대신 예외를 발생시키도록 변경 * style: HandlerMapper 에서 body 없는 응답 작성 메서드 네이밍 변경 * chore: 정적 필드와 인스턴스 필드 간의 공백추가 * refactor: Enum 의 name method 를 통해 HttpMethod 를 반환하도록 변경 * fix: Option.get 호출 전 isEmpty 를 호출하도록 변경
1 parent 68db530 commit e5a610c

30 files changed

+1389
-142
lines changed
Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,59 @@
11
package study;
22

3-
import org.junit.jupiter.api.DisplayName;
4-
import org.junit.jupiter.api.Test;
3+
import static org.assertj.core.api.Assertions.assertThat;
54

5+
import java.io.BufferedReader;
6+
import java.io.File;
7+
import java.io.IOException;
8+
import java.net.URISyntaxException;
9+
import java.nio.file.Files;
610
import java.nio.file.Path;
7-
import java.util.Collections;
811
import java.util.List;
9-
10-
import static org.assertj.core.api.Assertions.assertThat;
12+
import java.util.stream.Collectors;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Test;
1115

1216
/**
13-
* 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다.
14-
* File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
17+
* 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다.
1518
*/
1619
@DisplayName("File 클래스 학습 테스트")
1720
class FileTest {
1821

1922
/**
2023
* resource 디렉터리 경로 찾기
21-
*
22-
* File 객체를 생성하려면 파일의 경로를 알아야 한다.
23-
* 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다.
24-
* resource 디렉터리의 경로는 어떻게 알아낼 수 있을까?
24+
* <p>
25+
* File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수
26+
* 있을까?
2527
*/
2628
@Test
27-
void resource_디렉터리에_있는_파일의_경로를_찾는다() {
29+
void resource_디렉터리에_있는_파일의_경로를_찾는다() throws URISyntaxException {
2830
final String fileName = "nextstep.txt";
2931

3032
// todo
31-
final String actual = "";
33+
File file = new File(getClass().getClassLoader().getResource("nextstep.txt").toURI());
34+
final String actual = file.getName();
3235

3336
assertThat(actual).endsWith(fileName);
3437
}
3538

3639
/**
3740
* 파일 내용 읽기
38-
*
39-
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
40-
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
41+
* <p>
42+
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
4143
*/
4244
@Test
43-
void 파일의_내용을_읽는다() {
45+
void 파일의_내용을_읽는다() throws IOException {
4446
final String fileName = "nextstep.txt";
4547

4648
// todo
47-
final Path path = null;
49+
String file = getClass().getClassLoader().getResource(fileName).getFile();
50+
Path path = Path.of(file);
4851

4952
// todo
50-
final List<String> actual = Collections.emptyList();
53+
BufferedReader bufferedReader = Files.newBufferedReader(path);
54+
final List<String> actual = bufferedReader.lines().collect(Collectors.toList());
5155

56+
// then
5257
assertThat(actual).containsOnly("nextstep");
5358
}
5459
}

study/src/test/java/study/IOStreamTest.java

Lines changed: 150 additions & 66 deletions
Large diffs are not rendered by default.

tomcat/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Mission 1 : HTTP 서버 구현하기
2+
3+
1. [x] Get /index.html 응답하기
4+
2. [x] CSS 지원하기
5+
- [x] 정적 파일을 지원하는 ResourceProvider 를 생성한다.
6+
- [x] 해당 정적 파일을 읽어온다.
7+
- [x] 해당 정적 파일의 타입을 반환한다.
8+
3. [x] Query String 파싱
9+
- [x] 먼저 정적 파일을 찾는다.
10+
- [x] 요청을 handle 할 수 있는 핸들러를 찾는다.
11+
- [x] 아무것도 못 찾았다면 not Found 페이지 반환.
12+
13+
## Mission 2 : 로그인 구현하기
14+
15+
1. [x] HTTP Status Code 302
16+
2. [x] POST 방식으로 회원가입 및 로그인
17+
- [x] Request 를 생성한다
18+
- [x] 요청을 해결할 수 있는 Contoller 를 Mapper 로 엮어둔다.
19+
3. [x] Cookie에 JSESSIONID 값 저장하기
20+
4. [x] Session 구현하기
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
package nextstep.jwp.db;
22

3-
import nextstep.jwp.model.User;
4-
53
import java.util.Map;
64
import java.util.Optional;
75
import java.util.concurrent.ConcurrentHashMap;
6+
import nextstep.jwp.model.User;
87

98
public class InMemoryUserRepository {
109

1110
private static final Map<String, User> database = new ConcurrentHashMap<>();
11+
private static Long publishedId = 1L;
1212

1313
static {
1414
final User user = new User(1L, "gugu", "password", "[email protected]");
1515
database.put(user.getAccount(), user);
1616
}
1717

18-
public static void save(User user) {
19-
database.put(user.getAccount(), user);
18+
private InMemoryUserRepository() {
2019
}
2120

2221
public static Optional<User> findByAccount(String account) {
2322
return Optional.ofNullable(database.get(account));
2423
}
2524

26-
private InMemoryUserRepository() {}
25+
public static void save(User user) {
26+
User saveUser = new User(++publishedId, user.getAccount(), user.getPassword(), user.getEmail());
27+
database.put(saveUser.getAccount(), saveUser);
28+
}
2729
}

tomcat/src/main/java/nextstep/jwp/model/User.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,29 @@ public boolean checkPassword(String password) {
2222
return this.password.equals(password);
2323
}
2424

25+
public Long getId() {
26+
return id;
27+
}
28+
2529
public String getAccount() {
2630
return account;
2731
}
2832

33+
public String getPassword() {
34+
return password;
35+
}
36+
37+
public String getEmail() {
38+
return email;
39+
}
40+
2941
@Override
3042
public String toString() {
3143
return "User{" +
32-
"id=" + id +
33-
", account='" + account + '\'' +
34-
", email='" + email + '\'' +
35-
", password='" + password + '\'' +
36-
'}';
44+
"id=" + id +
45+
", account='" + account + '\'' +
46+
", email='" + email + '\'' +
47+
", password='" + password + '\'' +
48+
'}';
3749
}
3850
}
Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
package org.apache.coyote.http11;
22

3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.net.Socket;
37
import nextstep.jwp.exception.UncheckedServletException;
48
import org.apache.coyote.Processor;
9+
import org.apache.coyote.http11.controller.HandlerMapper;
10+
import org.apache.coyote.http11.request.HttpRequest;
511
import org.slf4j.Logger;
612
import org.slf4j.LoggerFactory;
713

8-
import java.io.IOException;
9-
import java.net.Socket;
10-
1114
public class Http11Processor implements Runnable, Processor {
1215

1316
private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
1417

1518
private final Socket connection;
19+
private final ResourceProvider resourceProvider;
20+
private final HandlerMapper handlerMapper;
1621

1722
public Http11Processor(final Socket connection) {
1823
this.connection = connection;
24+
this.resourceProvider = new ResourceProvider();
25+
this.handlerMapper = new HandlerMapper(this.resourceProvider);
1926
}
2027

2128
@Override
@@ -26,22 +33,29 @@ public void run() {
2633

2734
@Override
2835
public void process(final Socket connection) {
29-
try (final var inputStream = connection.getInputStream();
30-
final var outputStream = connection.getOutputStream()) {
31-
32-
final var responseBody = "Hello world!";
33-
34-
final var response = String.join("\r\n",
35-
"HTTP/1.1 200 OK ",
36-
"Content-Type: text/html;charset=utf-8 ",
37-
"Content-Length: " + responseBody.getBytes().length + " ",
38-
"",
39-
responseBody);
40-
36+
try (final var inputReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
37+
final var outputStream = connection.getOutputStream()) {
38+
HttpRequest httpRequest = HttpRequest.makeRequest(inputReader);
39+
String response = getResponse(httpRequest);
4140
outputStream.write(response.getBytes());
4241
outputStream.flush();
4342
} catch (IOException | UncheckedServletException e) {
4443
log.error(e.getMessage(), e);
4544
}
4645
}
46+
47+
private String getResponse(HttpRequest httpRequest) {
48+
if (resourceProvider.haveResource(httpRequest.getRequestLine().getPath())) {
49+
return resourceProvider.staticResourceResponse(httpRequest.getRequestLine().getPath());
50+
}
51+
if (handlerMapper.haveAvailableHandler(httpRequest)) {
52+
return handlerMapper.controllerResponse(httpRequest);
53+
}
54+
return String.join("\r\n",
55+
"HTTP/1.1 200 OK ",
56+
"Content-Type: text/html;charset=utf-8 ",
57+
"Content-Length: 12 ",
58+
"",
59+
"Hello world!");
60+
}
4761
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.apache.coyote.http11;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.net.URL;
6+
import java.nio.file.Files;
7+
import java.util.Optional;
8+
9+
public class ResourceProvider {
10+
11+
public boolean haveResource(String resourcePath) {
12+
return findFileURL(resourcePath).isPresent();
13+
}
14+
15+
public String resourceBodyOf(String resourcePath) {
16+
try {
17+
Optional<URL> fileURL = findFileURL(resourcePath);
18+
if (fileURL.isPresent()) {
19+
URL url = fileURL.get();
20+
return new String(Files.readAllBytes(new File(url.getFile()).toPath()));
21+
}
22+
throw new IllegalArgumentException("파일이 존재하지 않습니다.");
23+
} catch (IOException e) {
24+
throw new IllegalArgumentException("존재하지 않는 파일 입니다.");
25+
}
26+
}
27+
28+
private Optional<URL> findFileURL(String resourcePath) {
29+
URL resourceURL = getClass().getClassLoader().getResource("static" + resourcePath);
30+
if (resourceURL == null) {
31+
return Optional.empty();
32+
}
33+
if (new File(resourceURL.getFile()).isDirectory()) {
34+
return Optional.empty();
35+
}
36+
return Optional.of(resourceURL);
37+
}
38+
39+
public String contentTypeOf(String resourcePath) {
40+
File file = getFile(resourcePath);
41+
String fileName = file.getName();
42+
if (fileName.endsWith(".js")) {
43+
return "Content-Type: text/javascript ";
44+
}
45+
if (fileName.endsWith(".css")) {
46+
return "Content-Type: text/css;charset=utf-8";
47+
}
48+
if (fileName.endsWith(".html")) {
49+
return "Content-Type: text/html;charset=utf-8 ";
50+
}
51+
return "Content-Type: text/plain";
52+
}
53+
54+
private File getFile(String resourcePath) {
55+
return new File(
56+
findFileURL(resourcePath).orElseThrow((() -> new IllegalArgumentException("파일이 존재하지 않습니다.")))
57+
.getFile());
58+
}
59+
60+
public String staticResourceResponse(String resourcePath) {
61+
String responseBody = resourceBodyOf(resourcePath);
62+
return String.join("\r\n",
63+
"HTTP/1.1 200 OK ",
64+
contentTypeOf(resourcePath),
65+
"Content-Length: " + responseBody.getBytes().length + " ",
66+
"",
67+
responseBody);
68+
}
69+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.apache.coyote.http11.controller;
2+
3+
import org.apache.coyote.http11.request.HttpRequest;
4+
import org.apache.coyote.http11.response.HttpResponse;
5+
6+
public interface Controller {
7+
8+
HttpResponse<? extends Object> handle(HttpRequest httpRequest);
9+
}

0 commit comments

Comments
 (0)