Skip to content

Commit 112d989

Browse files
authored
[톰캣 구현하기 1, 2단계] 로이(김덕우) 미션 제출합니다. (#396)
* test: IOStream 학습 테스트 * test: File 학습 테스트 * feat: Request 구성 요소 구현 * feat: HttpRequest 구현 * feat: Response 구성 요소 구현 * feat: HttpResponse 구현 * feat: HttpProcessor 구현 * refactor: HttpRequest 관련 로직 구조 변경 * feat: 정적 리소스 관련 controller 구현 * feat: 로그인 및 회원가입 관련 controller 구현 * feat: 쿠키 및 세션 구현 * feat: Request 매핑 및 쿼리 파라미터 파싱 로직 구현 * refactor: processor 로직 수정
1 parent 68db530 commit 112d989

26 files changed

+909
-33
lines changed

study/src/test/java/study/FileTest.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
import org.junit.jupiter.api.DisplayName;
44
import org.junit.jupiter.api.Test;
55

6+
import java.io.BufferedReader;
7+
import java.io.File;
8+
import java.io.FileInputStream;
9+
import java.io.FileNotFoundException;
10+
import java.io.InputStreamReader;
11+
import java.net.URISyntaxException;
12+
import java.net.URL;
613
import java.nio.file.Path;
714
import java.util.Collections;
815
import java.util.List;
16+
import java.util.stream.Collectors;
917

1018
import static org.assertj.core.api.Assertions.assertThat;
1119

@@ -28,7 +36,8 @@ class FileTest {
2836
final String fileName = "nextstep.txt";
2937

3038
// todo
31-
final String actual = "";
39+
final URL url = getClass().getClassLoader().getResource(fileName);
40+
final String actual = url.getPath();
3241

3342
assertThat(actual).endsWith(fileName);
3443
}
@@ -40,14 +49,18 @@ class FileTest {
4049
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
4150
*/
4251
@Test
43-
void 파일의_내용을_읽는다() {
52+
void 파일의_내용을_읽는다() throws URISyntaxException, FileNotFoundException {
4453
final String fileName = "nextstep.txt";
4554

4655
// todo
47-
final Path path = null;
56+
final URL url = getClass().getClassLoader().getResource(fileName);
57+
final Path path = Path.of(url.toURI());
4858

4959
// todo
50-
final List<String> actual = Collections.emptyList();
60+
final File file = path.toFile();
61+
final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
62+
63+
final List<String> actual = reader.lines().collect(Collectors.toUnmodifiableList());
5164

5265
assertThat(actual).containsOnly("nextstep");
5366
}

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
import org.junit.jupiter.api.Test;
66

77
import java.io.*;
8+
import java.util.stream.Stream;
89

910
import static org.assertj.core.api.Assertions.assertThat;
1011
import static org.mockito.Mockito.*;
1112

1213
/**
1314
* 자바는 스트림(Stream)으로부터 I/O를 사용한다.
1415
* 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다.
15-
*
16+
* <p>
1617
* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다.
1718
* FilterStream은 InputStream이나 OutputStream에 연결될 수 있다.
1819
* FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환)
19-
*
20+
* <p>
2021
* Stream은 데이터를 바이트로 읽고 쓴다.
2122
* 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다.
2223
* Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다.
@@ -26,7 +27,7 @@ class IOStreamTest {
2627

2728
/**
2829
* OutputStream 학습하기
29-
*
30+
* <p>
3031
* 자바의 기본 출력 클래스는 java.io.OutputStream이다.
3132
* OutputStream의 write(int b) 메서드는 기반 메서드이다.
3233
* <code>public abstract void write(int b) throws IOException;</code>
@@ -39,7 +40,7 @@ class OutputStream_학습_테스트 {
3940
* OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다.
4041
* 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때,
4142
* 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다.
42-
*
43+
* <p>
4344
* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다.
4445
* <code>write(byte[] data)</code>와 <code>write(byte b[], int off, int len)</code> 메서드는
4546
* 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다.
@@ -53,7 +54,7 @@ class OutputStream_학습_테스트 {
5354
* todo
5455
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
5556
*/
56-
57+
outputStream.write(bytes);
5758
final String actual = outputStream.toString();
5859

5960
assertThat(actual).isEqualTo("nextstep");
@@ -63,7 +64,7 @@ class OutputStream_학습_테스트 {
6364
/**
6465
* 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다.
6566
* BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다.
66-
*
67+
* <p>
6768
* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자.
6869
* flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다.
6970
* Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면
@@ -79,6 +80,7 @@ class OutputStream_학습_테스트 {
7980
* ByteArrayOutputStream과 어떤 차이가 있을까?
8081
*/
8182

83+
outputStream.flush();
8284
verify(outputStream, atLeastOnce()).flush();
8385
outputStream.close();
8486
}
@@ -96,19 +98,23 @@ class OutputStream_학습_테스트 {
9698
* try-with-resources를 사용한다.
9799
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
98100
*/
101+
final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112};
102+
try (outputStream) {
103+
outputStream.write(bytes);
104+
}
99105

100106
verify(outputStream, atLeastOnce()).close();
101107
}
102108
}
103109

104110
/**
105111
* InputStream 학습하기
106-
*
112+
* <p>
107113
* 자바의 기본 입력 클래스는 java.io.InputStream이다.
108114
* InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다.
109115
* InputStream의 read() 메서드는 기반 메서드이다.
110116
* <code>public abstract int read() throws IOException;</code>
111-
*
117+
* <p>
112118
* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다.
113119
*/
114120
@Nested
@@ -128,7 +134,7 @@ class InputStream_학습_테스트 {
128134
* todo
129135
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
130136
*/
131-
final String actual = "";
137+
final String actual = new String(inputStream.readAllBytes());
132138

133139
assertThat(actual).isEqualTo("🤩");
134140
assertThat(inputStream.read()).isEqualTo(-1);
@@ -148,14 +154,17 @@ class InputStream_학습_테스트 {
148154
* try-with-resources를 사용한다.
149155
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
150156
*/
157+
try (inputStream){
158+
inputStream.readAllBytes();
159+
}
151160

152161
verify(inputStream, atLeastOnce()).close();
153162
}
154163
}
155164

156165
/**
157166
* FilterStream 학습하기
158-
*
167+
* <p>
159168
* 필터는 필터 스트림, reader, writer로 나뉜다.
160169
* 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다.
161170
* reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다.
@@ -172,9 +181,14 @@ class FilterStream_학습_테스트 {
172181
void 필터인_BufferedInputStream를_사용해보자() {
173182
final String text = "필터에 연결해보자.";
174183
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
175-
final InputStream bufferedInputStream = null;
184+
final InputStream bufferedInputStream = new BufferedInputStream(inputStream);
176185

177-
final byte[] actual = new byte[0];
186+
final byte[] actual;
187+
try (bufferedInputStream) {
188+
actual = bufferedInputStream.readAllBytes();
189+
} catch (IOException e) {
190+
throw new RuntimeException(e);
191+
}
178192

179193
assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
180194
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
@@ -204,9 +218,17 @@ class InputStreamReader_학습_테스트 {
204218
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
205219
"");
206220
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
221+
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
207222

208223
final StringBuilder actual = new StringBuilder();
209224

225+
try (bufferedReader) {
226+
final Stream<String> lines = bufferedReader.lines();
227+
lines.forEach(line -> actual.append(line).append("\r\n"));
228+
} catch (IOException e) {
229+
throw new RuntimeException(e);
230+
}
231+
210232
assertThat(actual).hasToString(emoji);
211233
}
212234
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package nextstep.jwp.controller;
2+
3+
import org.apache.coyote.http11.request.HttpMethod;
4+
import org.apache.coyote.http11.request.HttpRequest;
5+
import org.apache.coyote.http11.response.ContentType;
6+
import org.apache.coyote.http11.response.HttpResponse;
7+
import org.apache.coyote.http11.response.HttpStatus;
8+
import org.apache.coyote.http11.util.PathFinder;
9+
10+
import java.io.IOException;
11+
import java.net.URISyntaxException;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
15+
public abstract class AbstractController implements Controller {
16+
17+
@Override
18+
public HttpResponse service(HttpRequest request) throws Exception {
19+
if (request.getHttpMethod() == HttpMethod.GET) {
20+
return doGet(request);
21+
}
22+
return doPost(request);
23+
}
24+
25+
protected HttpResponse doGet(HttpRequest request) throws Exception {
26+
return defaultInternalServerErrorPage();
27+
}
28+
29+
protected HttpResponse doPost(HttpRequest request) throws Exception {
30+
return defaultInternalServerErrorPage();
31+
}
32+
33+
private HttpResponse defaultInternalServerErrorPage() throws URISyntaxException, IOException {
34+
final Path path = PathFinder.findPath("/500.html");
35+
final String responseBody = new String(Files.readAllBytes(path));
36+
return new HttpResponse(HttpStatus.INTERNAL_SERVER_ERROR, responseBody, ContentType.HTML);
37+
}
38+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package nextstep.jwp.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 service(HttpRequest request) throws Exception;
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package nextstep.jwp.controller;
2+
3+
import org.apache.coyote.http11.request.HttpRequest;
4+
import org.apache.coyote.http11.response.ContentType;
5+
import org.apache.coyote.http11.response.HttpResponse;
6+
import org.apache.coyote.http11.response.HttpStatus;
7+
import org.apache.coyote.http11.util.PathFinder;
8+
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
public class FileController extends AbstractController {
13+
14+
private static final int CONTENT_TYPE_START_INDEX = 1;
15+
16+
@Override
17+
protected HttpResponse doGet(HttpRequest request) throws Exception {
18+
String requestUrl = request.getRequestUrl();
19+
Path path = PathFinder.findPath(requestUrl);
20+
21+
String extension = requestUrl.split("\\.")[CONTENT_TYPE_START_INDEX];
22+
var responseBody = new String(Files.readAllBytes(path));
23+
ContentType contentType = ContentType.findContentType(extension);
24+
25+
return new HttpResponse(HttpStatus.OK, responseBody, contentType);
26+
}
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package nextstep.jwp.controller;
2+
3+
import org.apache.coyote.http11.request.HttpRequest;
4+
import org.apache.coyote.http11.response.ContentType;
5+
import org.apache.coyote.http11.response.HttpResponse;
6+
import org.apache.coyote.http11.response.HttpStatus;
7+
8+
public class HelloController extends AbstractController {
9+
10+
@Override
11+
protected HttpResponse doGet(HttpRequest request) {
12+
String responseBody = "Hello world!";
13+
return new HttpResponse(HttpStatus.OK, responseBody, ContentType.HTML);
14+
}
15+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package nextstep.jwp.controller;
2+
3+
import nextstep.jwp.db.InMemoryUserRepository;
4+
import nextstep.jwp.model.User;
5+
import org.apache.coyote.http11.request.HttpRequest;
6+
import org.apache.coyote.http11.response.ContentType;
7+
import org.apache.coyote.http11.response.HttpResponse;
8+
import org.apache.coyote.http11.response.HttpStatus;
9+
import org.apache.coyote.http11.session.Session;
10+
import org.apache.coyote.http11.session.SessionManager;
11+
import org.apache.coyote.http11.util.PathFinder;
12+
import org.apache.coyote.http11.util.QueryParamsParser;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
public class LoginController extends AbstractController {
23+
24+
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
25+
26+
@Override
27+
protected HttpResponse doGet(HttpRequest request) throws Exception {
28+
Path path = PathFinder.findPath("/login.html");
29+
String responseBody = new String(Files.readAllBytes(path));
30+
Optional<User> loginUser = request.findUserByJSessionId();
31+
if (loginUser.isPresent()) {
32+
return redirectByAlreadyLogin(responseBody);
33+
}
34+
return new HttpResponse(HttpStatus.OK, responseBody, ContentType.HTML);
35+
}
36+
37+
private HttpResponse redirectByAlreadyLogin(String responseBody) {
38+
return new HttpResponse(HttpStatus.FOUND, responseBody, ContentType.HTML, "/index.html");
39+
}
40+
41+
@Override
42+
protected HttpResponse doPost(HttpRequest request) throws Exception {
43+
HashMap<String, String> loginData = QueryParamsParser.parseByBody(request.getRequestBody());
44+
Path path = PathFinder.findPath("/login.html");
45+
String responseBody = new String(Files.readAllBytes(path));
46+
return makeLoginResponse(loginData, responseBody);
47+
}
48+
49+
private HttpResponse makeLoginResponse(Map<String, String> loginData, final String responseBody) {
50+
String account = loginData.get("account");
51+
String password = loginData.get("password");
52+
if (loginData.isEmpty() || !isSuccessLogin(account, password)) {
53+
log.info("로그인에 실패했습니다.");
54+
return failLoginResponse(responseBody);
55+
}
56+
User user = new User(account, password);
57+
return successLoginResponse(responseBody, user);
58+
}
59+
60+
private HttpResponse successLoginResponse(String responseBody, User user) {
61+
HttpResponse httpResponse =
62+
new HttpResponse(HttpStatus.FOUND, responseBody, ContentType.HTML, "/index.html");
63+
Session session = new Session();
64+
session.setAttribute("user", user);
65+
SessionManager.add(session);
66+
httpResponse.addJSessionId(session);
67+
return httpResponse;
68+
}
69+
70+
private HttpResponse failLoginResponse(String responseBody) {
71+
log.info("로그인 계정 정보가 이상합니다. responseBody={}", responseBody);
72+
return new HttpResponse(HttpStatus.FOUND, responseBody, ContentType.HTML, "/401.html");
73+
}
74+
75+
private boolean isSuccessLogin(String account, String password) {
76+
Optional<User> accessUser = InMemoryUserRepository.findByAccount(account);
77+
return accessUser.filter(user -> checkPassword(password, user))
78+
.isPresent();
79+
}
80+
81+
private boolean checkPassword(String password, User accessUser) {
82+
if (!accessUser.checkPassword(password)) {
83+
return false;
84+
}
85+
log.info("user = {}", accessUser);
86+
return true;
87+
}
88+
}

0 commit comments

Comments
 (0)