Skip to content

Commit

Permalink
[1단계 - Tomcat 구현하기] 알파카(최휘용) 미션 제출합니다. (#568)
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)

* feat: 정적 파일의 내용 GET 기능 구현

* feat: response에 Content-type 지정 기능 구현

* feat: HttpResponse 구현

* feat: HttpRequest 구현

* feat: cache 학습 테스트 코드 구현

* feat: GET login 기능 구현

* feat: cache 테스트 4번 구현

* feat: header 필드 및 파싱 기능 구현

---------

Co-authored-by: Gyeongho Yang <[email protected]>
  • Loading branch information
slimsha2dy and geoje authored Sep 6, 2024
1 parent 0b698a2 commit d45988c
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 38 deletions.
2 changes: 1 addition & 1 deletion study/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ 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'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.assertj:assertj-core:3.26.0'
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,12 +1,20 @@
package cache.com.example.etag;

import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.ShallowEtagHeaderFilter;

@Configuration
public class EtagFilterConfiguration {

// @Bean
// public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
// return null;
// }
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean =
new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/etag/*", PREFIX_STATIC_RESOURCES + "/*");
return filterRegistrationBean;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cache.com.example.version;

import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -20,6 +22,9 @@ public CacheBustingWebConfig(ResourceVersion version) {
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**")
.addResourceLocations("classpath:/static/");
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)
.cachePublic());
;
}
}
4 changes: 4 additions & 0 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ server:
accept-count: 1
max-connections: 1
threads:
min-spare: 2
max: 2
compression:
enabled: true
min-response-size: 10
16 changes: 14 additions & 2 deletions study/src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@
<html lang="ko">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
</head>
<body>
Hello, World!
Hello, World<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
name="viewport">
<meta content="ie=edge" http-equiv="X-UA-Compatible">
<title>Document</title>
</head>
<body>

</body>
</html>
</body>
</html>
16 changes: 6 additions & 10 deletions study/src/test/java/cache/com/example/GreetingControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cache.com.example;

import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES;

import cache.com.example.version.ResourceVersion;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -10,10 +13,6 @@
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.time.Duration;

import static cache.com.example.version.CacheBustingWebConfig.PREFIX_STATIC_RESOURCES;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingControllerTest {

Expand All @@ -29,7 +28,7 @@ class GreetingControllerTest {
void testNoCachePrivate() {
final var response = webTestClient
.get()
.uri("/")
.uri("/cache-control")
.exchange()
.expectStatus().isOk()
.expectHeader().cacheControl(CacheControl.noCache().cachePrivate())
Expand All @@ -45,7 +44,6 @@ void testCompression() {
.uri("/")
.exchange()
.expectStatus().isOk()

// gzip으로 요청 보내도 어떤 방식으로 압축할지 서버에서 결정한다.
// 웹브라우저에서 localhost:8080으로 접근하면 응답 헤더에 "Content-Encoding: gzip"이 있다.
.expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked")
Expand All @@ -68,10 +66,8 @@ void testETag() {
}

/**
* http://localhost:8080/resource-versioning
* 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다.
* 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다.
* 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다.
* http://localhost:8080/resource-versioning 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. 보통 정적 파일을 캐싱 무효화하기
* 위해 캐싱과 함께 버전을 적용시킨다. 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다.
*/
@Test
void testCacheBustingOfStaticResources() {
Expand Down
26 changes: 26 additions & 0 deletions tomcat/src/main/java/com/techcourse/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.techcourse.controller;

import com.techcourse.db.InMemoryUserRepository;
import org.apache.coyote.http11.HttpRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Controller {

private static final Logger LOGGER = LoggerFactory.getLogger(Controller.class);

public Controller() {
}

public String getLogin(HttpRequest request) {
String requestAccount = request.getQueryStringData("account");
String requestPassword = request.getQueryStringData("password");
InMemoryUserRepository.findByAccount(requestAccount)
.ifPresent(user -> {
if (user.checkPassword(requestPassword)) {
LOGGER.info(user.toString());
}
});
return "/login.html";
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.techcourse.db;

import com.techcourse.model.User;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -15,13 +14,17 @@ public class InMemoryUserRepository {
database.put(user.getAccount(), user);
}

private InMemoryUserRepository() {
}

public static void save(User user) {
database.put(user.getAccount(), user);
}

public static Optional<User> findByAccount(String account) {
if (account == null) {
return Optional.empty();
}
return Optional.ofNullable(database.get(account));
}

private InMemoryUserRepository() {}
}
117 changes: 102 additions & 15 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
package org.apache.coyote.http11;

import com.techcourse.exception.UncheckedServletException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.coyote.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.Socket;

public class Http11Processor implements Runnable, Processor {

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

private final Socket connection;
private final RequestMapper requestMapper;

public Http11Processor(final Socket connection) {
this.connection = connection;
this.requestMapper = new RequestMapper();
}

@Override
Expand All @@ -25,23 +38,97 @@ public void run() {
}

@Override
public void process(final Socket connection) {
try (final var inputStream = connection.getInputStream();
final var outputStream = connection.getOutputStream()) {

final var responseBody = "Hello world!";
public void process(Socket connection) {
try (InputStream inputStream = connection.getInputStream();
OutputStream outputStream = connection.getOutputStream()) {
HttpRequest request = parseInput(inputStream);

final var response = String.join("\r\n",
"HTTP/1.1 200 OK ",
"Content-Type: text/html;charset=utf-8 ",
"Content-Length: " + responseBody.getBytes().length + " ",
"",
responseBody);
String resourceName = requestMapper.requestMapping(request);
String responseBody = parseStartLine(resourceName);
String resourceExtension = getExtension(resourceName);

HttpResponse response = new HttpResponse(responseBody, resourceExtension);
outputStream.write(response.getBytes());
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
} catch (IOException | UncheckedServletException | URISyntaxException e) {
log.error(e.getMessage(), e);
}
}

private HttpRequest parseInput(InputStream inputStream) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String tokens[] = bufferedReader.readLine()
.split(" ");
if (tokens.length != 3) {
throw new RuntimeException();
}

String path = parsePath(tokens[1]);
Map<String, String> queryString = parseQueryString(tokens[1]);
Map<String, String> headers = parseHeaders(bufferedReader);
return new HttpRequest(tokens[0], path, queryString, tokens[2], headers);
}

private String parsePath(String token) {
int separatorIndex = token.indexOf('?');
if (separatorIndex == -1) {
return token;
}
return token.substring(0, separatorIndex);
}

private Map<String, String> parseQueryString(String token) {
Map<String, String> queryString = new HashMap<>();
int separatorIndex = token.indexOf('?');
if (separatorIndex == -1) {
return queryString;
}
Stream.of(token.substring(separatorIndex + 1)
.split("&"))
.forEach(data -> parseData(data, queryString));
return queryString;
}

private void parseData(String s, Map<String, String> queryString) {
String data[] = s.split("=");
if (data.length == 2) {
queryString.put(data[0], data[1]);
}
}

private String getExtension(String resourceName) {
int extensionIndex = resourceName.indexOf('.') + 1;
if (extensionIndex == 0) {
return "html";
}
return resourceName.substring(extensionIndex);
}

private String parseStartLine(String resourceName) throws URISyntaxException, IOException {
URL resource = getClass().getClassLoader()
.getResource("static" + resourceName);

try {
return Files.readString(Paths.get(resource.toURI()));
} catch (IOException e) {
return "Hello world!";
}
}

private Map<String, String> parseHeaders(BufferedReader bufferedReader) throws IOException {
String line = bufferedReader.readLine();
Map<String, String> headers = new LinkedHashMap<>();

while (!"".equals(line)) {
if (line == null) {
return headers;
}
int index = line.indexOf(':');
String key = line.substring(0, index);
String value = line.substring(index + 2);
headers.put(key, value);
line = bufferedReader.readLine();
}
return headers;
}
}
33 changes: 33 additions & 0 deletions tomcat/src/main/java/org/apache/coyote/http11/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.apache.coyote.http11;

import java.util.Map;

public class HttpRequest {

private final String method;
private final String path;
private final Map<String, String> queryString;
private final String protocolVersion;
private final Map<String, String> headers;

public HttpRequest(String method, String path, Map<String, String> queryString, String protocolVersion,
Map<String, String> headers) {
this.method = method;
this.path = path;
this.queryString = queryString;
this.protocolVersion = protocolVersion;
this.headers = headers;
}

public String getQueryStringData(String input) {
return queryString.get(input);
}

public String getPath() {
return this.path;
}

public String getMethod() {
return this.method;
}
}
Loading

0 comments on commit d45988c

Please sign in to comment.