Skip to content

Commit

Permalink
[1 - 2 단계 Tomcat 구현하기] 테드(김규태) 미션 제출합니다. (#572)
Browse files Browse the repository at this point in the history
* fix: remove implementation logback-classic on gradle (#501)

* fix: add threads min-spare configuration on properties (#502)

* test: 스터디 테스트 작성

* refactor: 자동 정렬 이전 파일로 교체

* refactor: import 변경

* refactor: 요청에 따라 자원을 반환하도록 변경

* feat: HttpRequest 추가

* feat: HttpResponse 추가

* refactor: Http11Processor에서 요청과 응답 분리

* refactor: 로그인 메서드 post로 변경

* refactor: HttpRequest에 requestBody 추가

* refactor: HttpResponse에 상태코드 추가

* feat: 요청을 처리하는 컨트롤러 구현

* feat: 매핑된 컨트롤러를 찾아서 반환하는 RequestMapping 추가

* feat: ViewResolver 및 View 추가

* refactor: Http11Processor에서 Controller를 사용하도록 변경

* refactor: HttpRequest 에서 key 값으로 찾도록 변경

* refactor: HttpResponse 에 쿠키를 포함하는 기능 추가

* feat: 쿠키 및 세션 추가

* refactor: 로그인 시, 세션 확인하는 기능 추가

* refactor: ReqeustParser를 통해 HttpRequest 를 만들도록 변경

* refactor: HttpRequest 관련 클래스 분리

* refactor: RequestMapping 싱글톤으로 변경

* refactor: RegisterController에서 request의 기능을 활용하도록 변경

* refactor: 로깅 추가 및 수정

* refactor: Accpet로 response의 ContentType 구성하도록 변경

* refactor: 정의되지 않은 요청 시, 404 페이지 반환하도록 변경

* refactor: 사용하지 않는 주석 제거

* refactor: 패키지 구조 변경

* refactor: post 요청에서 body가 존재하지 않을 경우 400을 반환하도록 변경

* refactor: cookie에 대한 NPE 처리

* refactor: SessionManager 를 static 으로 활용하도록 변경

---------

Co-authored-by: Gyeongho Yang <[email protected]>
  • Loading branch information
Kimprodp and geoje authored Sep 10, 2024
1 parent 0b698a2 commit 5c73e56
Show file tree
Hide file tree
Showing 26 changed files with 760 additions and 40 deletions.
1 change: 0 additions & 1 deletion study/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'ch.qos.logback:logback-classic:1.5.7'
implementation 'org.apache.commons:commons-lang3:3.14.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package cache.com.example;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import jakarta.servlet.http.HttpServletResponse;

@Controller
public class GreetingController {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package cache.com.example.version;

import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.stereotype.Component;

@Component
public class ResourceVersion {
Expand Down
1 change: 1 addition & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ server:
accept-count: 1
max-connections: 1
threads:
min-spare: 2
max: 2
37 changes: 19 additions & 18 deletions study/src/test/java/study/FileTest.java
Original file line number Diff line number Diff line change
@@ -1,53 +1,54 @@
package study;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

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

// todo
final String actual = "";
URL resource = getClass().getClassLoader().getResource(fileName);
final String actual = resource.getPath();

assertThat(actual).endsWith(fileName);
}

/**
* 파일 내용 읽기
*
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다.
* File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
* <p>
* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자.
*/
@Test
void 파일의_내용을_읽는다() {
void 파일의_내용을_읽는다() throws IOException, URISyntaxException {
final String fileName = "nextstep.txt";

// todo
final Path path = null;
URL resource = getClass().getClassLoader().getResource(fileName);
final Path path = Path.of(resource.toURI());

// todo
final List<String> actual = Collections.emptyList();
final List<String> actual = Files.readAllLines(path);

assertThat(actual).containsOnly("nextstep");
}
Expand Down
32 changes: 27 additions & 5 deletions study/src/test/java/study/IOStreamTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package study;

import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -54,6 +55,7 @@ class OutputStream_학습_테스트 {
* OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다
*/

outputStream.write(bytes);
final String actual = outputStream.toString();

assertThat(actual).isEqualTo("nextstep");
Expand All @@ -78,7 +80,7 @@ class OutputStream_학습_테스트 {
* flush를 사용해서 테스트를 통과시킨다.
* ByteArrayOutputStream과 어떤 차이가 있을까?
*/

outputStream.flush();
verify(outputStream, atLeastOnce()).flush();
outputStream.close();
}
Expand All @@ -96,6 +98,9 @@ class OutputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try(outputStream) {
outputStream.flush();
}

verify(outputStream, atLeastOnce()).close();
}
Expand Down Expand Up @@ -128,7 +133,7 @@ class InputStream_학습_테스트 {
* todo
* inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까?
*/
final String actual = "";
final String actual = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

assertThat(actual).isEqualTo("🤩");
assertThat(inputStream.read()).isEqualTo(-1);
Expand All @@ -148,6 +153,9 @@ class InputStream_학습_테스트 {
* try-with-resources를 사용한다.
* java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다.
*/
try(inputStream) {

}

verify(inputStream, atLeastOnce()).close();
}
Expand All @@ -169,12 +177,13 @@ class FilterStream_학습_테스트 {
* 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까?
*/
@Test
void 필터인_BufferedInputStream를_사용해보자() {
void 필터인_BufferedInputStream를_사용해보자() throws IOException {
final String text = "필터에 연결해보자.";
final InputStream inputStream = new ByteArrayInputStream(text.getBytes());
final InputStream bufferedInputStream = null;
final InputStream bufferedInputStream = new BufferedInputStream(inputStream);

final byte[] actual = new byte[0];
final byte[] actual = bufferedInputStream.readAllBytes();
bufferedInputStream.close();

assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class);
assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes());
Expand Down Expand Up @@ -204,9 +213,22 @@ class InputStreamReader_학습_테스트 {
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

final StringBuilder actual = new StringBuilder();

try(BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
while (true) {
String read = bufferedReader.readLine();
if (read == null) {
break;
}
actual.append(read).append("\r\n");
}
} catch (IOException e) {
e.printStackTrace();
}

assertThat(actual).hasToString(emoji);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.techcourse.controller;

import java.io.IOException;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public abstract class AbstractController implements Controller {

@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {
String method = request.getRequestLine().getMethod();
if (method.equals("POST")) {
doPost(request, response);
} else if (method.equals("GET")) {
doGet(request, response);
} else {
response.setStatus405();
}
}

protected void doPost(HttpRequest request, HttpResponse response) throws IOException {
}

protected void doGet(HttpRequest request, HttpResponse response) throws IOException {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.techcourse.controller;

import java.io.IOException;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public interface Controller {
void service(HttpRequest request, HttpResponse response) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.techcourse.controller;

import com.techcourse.db.InMemoryUserRepository;
import com.techcourse.model.User;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.techcourse.session.Session;
import com.techcourse.session.SessionManager;
import com.techcourse.view.View;
import com.techcourse.view.ViewResolver;
import org.apache.coyote.http11.HttpCookie;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;

public class LoginController extends AbstractController {

@Override
protected void doGet(HttpRequest request, HttpResponse response) throws IOException {
Session session = extractSession(request);
if (session != null) {
User user = (User) session.getAttribute("user");
if (user != null) {
responseLoginSuccess(response, session);
return;
}
}
responseLoginPage(request, response);
}

@Override
protected void doPost(HttpRequest request, HttpResponse response) {
if (!request.hasBodyData()) {
throw new IllegalArgumentException("Query string is missing in the request");
}

Map<String, String> requestFormData = request.getFormData();
String userName = requestFormData.get("account");
String password = requestFormData.get("password");

Optional<User> account = InMemoryUserRepository.findByAccount(userName);
if (account.isEmpty()) {
responseLoginFail(response);
} else {
User user = account.get();
if (user.checkPassword(password)) {
Session session = saveSession(user);
responseLoginSuccess(response, session);
} else {
responseLoginFail(response);
}
}
}

private Session extractSession(HttpRequest request) {
String cookie = request.getCookie();
if (cookie == null) {
Map<String, String> cookies = new HashMap<>();
for (String cookieParts : cookie.split(" ")) {
String[] keyAndValue = cookieParts.split("=");
cookies.put(keyAndValue[0], keyAndValue[1]);
}

String jsessionId = cookies.get("JSESSIONID");
return SessionManager.findSession(jsessionId);
}

return null;
}

private Session saveSession(User user) {
Session session = new Session();
session.setAttribute("user", user);
SessionManager.add(session);
return session;
}

private void responseLoginPage(HttpRequest request, HttpResponse response) throws IOException {
View view = ViewResolver.getView("/login.html");
response.setStatus200();
response.setResponseBody(view.getContent());
response.setContentType(request.getContentType());
}

private void responseLoginSuccess(HttpResponse response, Session session) {
response.setStatus302();
response.setLocation("/index.html");
response.setCookie(HttpCookie.ofJSessionId(session.getId()));
}

private void responseLoginFail(HttpResponse response) {
response.setStatus302();
response.setLocation("/401.html");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.techcourse.controller;

import com.techcourse.db.InMemoryUserRepository;
import com.techcourse.model.User;
import java.io.IOException;
import java.util.Map;
import com.techcourse.view.View;
import com.techcourse.view.ViewResolver;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RegisterController extends AbstractController {

private static final Logger log = LoggerFactory.getLogger(RegisterController.class);

@Override
protected void doPost(HttpRequest request, HttpResponse response) {
try {
if (!request.hasBodyData()) {
throw new IllegalArgumentException("RequestBody is missing in the request");
}

Map<String, String> requestFormData = request.getFormData();
String account = requestFormData.get("account");
String password = requestFormData.get("password");
String email = requestFormData.get("email");

User user = new User(account, password, email);
InMemoryUserRepository.save(user);
responseRegisterSuccess(response);

} catch (IllegalArgumentException e) {
response.setStatus400();
response.setResponseBody(e.getMessage());
log.info("Bad Request: {}", e.getMessage());

}
}

@Override
protected void doGet(HttpRequest request, HttpResponse response) throws IOException {
responseRegisterPage(request, response);
}

private void responseRegisterSuccess(HttpResponse response) {
response.setStatus302();
response.setLocation("/index.html");
}

private void responseRegisterPage(HttpRequest request, HttpResponse response) throws IOException {
View view = ViewResolver.getView("/register.html");
response.setStatus200();
response.setResponseBody(view.getContent());
response.setContentType(request.getContentType());
}
}
Loading

0 comments on commit 5c73e56

Please sign in to comment.