diff --git a/study/build.gradle b/study/build.gradle index 87a1f0313c..7bdce4f751 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 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' diff --git a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java index 305b1f1e1e..678cfd3aac 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,13 +1,18 @@ package cache.com.example.cachecontrol; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.mvc.WebContentInterceptor; @Configuration public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); + webContentInterceptor.setCacheControl(CacheControl.noCache().cachePrivate()); + registry.addInterceptor(webContentInterceptor); } } diff --git a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java index 41ef7a3d9a..96699d1ebd 100644 --- a/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java +++ b/study/src/main/java/cache/com/example/etag/EtagFilterConfiguration.java @@ -1,12 +1,20 @@ package cache.com.example.etag; +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() { -// return null; -// } + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag/*"); + filterRegistrationBean.addUrlPatterns("/resources/*"); + filterRegistrationBean.setName("etagFilter"); + return filterRegistrationBean; + } } diff --git a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java index 6da6d2c795..c7addcdab4 100644 --- a/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java +++ b/study/src/main/java/cache/com/example/version/CacheBustingWebConfig.java @@ -1,7 +1,9 @@ package cache.com.example.version; +import java.time.Duration; 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; @@ -20,6 +22,7 @@ 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(Duration.ofDays(365)).cachePublic()); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index e3503a5fb9..4c735aea11 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -2,6 +2,9 @@ handlebars: suffix: .html server: + compression: + enabled: true + min-response-size: 10 tomcat: accept-count: 1 max-connections: 1 diff --git a/study/src/test/java/cache/com/example/GreetingControllerTest.java b/study/src/test/java/cache/com/example/GreetingControllerTest.java index 9ce2a394f7..36314b50d4 100644 --- a/study/src/test/java/cache/com/example/GreetingControllerTest.java +++ b/study/src/test/java/cache/com/example/GreetingControllerTest.java @@ -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; @@ -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 { @@ -45,7 +44,6 @@ void testCompression() { .uri("/") .exchange() .expectStatus().isOk() - // gzip으로 요청 보내도 어떤 방식으로 압축할지 서버에서 결정한다. // 웹브라우저에서 localhost:8080으로 접근하면 응답 헤더에 "Content-Encoding: gzip"이 있다. .expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked") @@ -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() { diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..0d1043cfd3 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,50 @@ 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.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 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = this.getClass().getClassLoader().getResource(fileName).getFile(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + final Path path = Path.of(this.getClass().getClassLoader().getResource(fileName).getPath()); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..b102870711 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,51 @@ package study; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.stream.Collectors; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.io.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - /** - * 자바는 스트림(Stream)으로부터 I/O를 사용한다. - * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * - * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. - * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. - * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * - * Stream은 데이터를 바이트로 읽고 쓴다. - * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. - * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. + * 자바는 스트림(Stream)으로부터 I/O를 사용한다. 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. + *

+ * InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. FilterStream은 읽거나 쓰는 + * 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) + *

+ * Stream은 데이터를 바이트로 읽고 쓴다. 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 + * 처리할 수 있다. */ @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { /** * OutputStream 학습하기 - * - * 자바의 기본 출력 클래스는 java.io.OutputStream이다. - * OutputStream의 write(int b) 메서드는 기반 메서드이다. + *

+ * 자바의 기본 출력 클래스는 java.io.OutputStream이다. OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; */ @Nested class OutputStream_학습_테스트 { /** - * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. - * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. - * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, - * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + * OutputStream은 다른 매체에 바이트로 데이터를 쓸 때 사용한다. OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 + * 사용한다. 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 + * 사용한다. + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -53,7 +59,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ - + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -61,13 +67,10 @@ class OutputStream_학습_테스트 { } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { @@ -78,14 +81,13 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ - + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -96,27 +98,26 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ - + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } } /** * InputStream 학습하기 - * - * 자바의 기본 입력 클래스는 java.io.InputStream이다. - * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. - * InputStream의 read() 메서드는 기반 메서드이다. + *

+ * 자바의 기본 입력 클래스는 java.io.InputStream이다. InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. InputStream의 read() 메서드는 기반 + * 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested class InputStream_학습_테스트 { /** - * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. - * int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. + * read() 메서드는 매체로부터 단일 바이트를 읽는데, 0부터 255 사이의 값을 int 타입으로 반환한다. int 값을 byte 타입으로 변환하면 -128부터 127 사이의 값으로 변환된다. * 그리고 Stream 끝에 도달하면 -1을 반환한다. */ @Test @@ -128,7 +129,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +137,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -148,33 +148,32 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ - + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } } /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @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(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -182,19 +181,15 @@ class FilterStream_학습_테스트 { } /** - * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. - * 문자열이 아닌 바이트 단위로 처리하려니 불편하다. - * 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. - * reader, writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. - * 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. + * 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다. 문자열이 아닌 바이트 단위로 처리하려니 불편하다. 그리고 바이트를 문자(char)로 처리하려면 인코딩을 신경 써야 한다. reader, + * writer를 사용하면 입출력 스트림을 바이트가 아닌 문자 단위로 데이터를 처리하게 된다. 그리고 InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. */ @Nested class InputStreamReader_학습_테스트 { /** - * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. - * 읽어온 문자(char)를 문자열(String)로 처리하자. - * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. + * InputStreamReader를 사용해서 바이트를 문자(char)로 읽어온다. 읽어온 문자(char)를 문자열(String)로 처리하자. 필터인 BufferedReader를 사용하면 + * readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test void BufferedReader를_사용하여_문자열을_읽어온다() { @@ -204,8 +199,10 @@ class InputStreamReader_학습_테스트 { "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); - final StringBuilder actual = new StringBuilder(); + final String actual = bufferedReader.lines() + .collect(Collectors.joining("\r\n")) + "\r\n"; assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/org/apache/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java new file mode 100644 index 0000000000..e8997b3c0d --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,34 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(final String name) { + return values.get(name); + } + + public void setAttribute(final String name, final Object value) { + values.put(name, value); + } + + public void removeAttribute(final String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java new file mode 100644 index 0000000000..9ee4edf47e --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,25 @@ +package org.apache.catalina.session; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + public static void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public static Session findSession(final String id) throws IOException { + return SESSIONS.get(id); + } + + public static void remove(final Session session) { + SESSIONS.remove(session.getId()); + } + + private SessionManager() { + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/FileType.java b/tomcat/src/main/java/org/apache/coyote/http11/FileType.java new file mode 100644 index 0000000000..f28fb7a789 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/FileType.java @@ -0,0 +1,17 @@ +package org.apache.coyote.http11; + +public enum FileType { + HTML("text/html"), + JAVASCRIPT("text/javascript"), + CSS("text/css"); + + private final String value; + + FileType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index bb14184757..51cdc3b626 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,16 +1,34 @@ package org.apache.coyote.http11; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.techcourse.db.InMemoryUserRepository; import com.techcourse.exception.UncheckedServletException; +import com.techcourse.model.User; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.UUID; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { - + public static final String HTML_SUFFIX = ".html"; private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + public static final String JSESSIONID = "JSESSIONID"; + public static final String STATIC_PATH = "static"; + public static final String INDEX_PAGE = "/index.html"; + public static final String ACCESS_DENIED_PAGE = "/401.html"; private final Socket connection; @@ -29,19 +47,138 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8)); + final var request = HttpRequest.from(reader); + if (request.getRequestLine() == null) { + return; + } + + final var method = request.getRequestMethod(); + final var url = request.getRequestPath(); + + log.info("request method: {}, request url: {}", method, url); + + if ("/".equals(url)) { + outputStream.write(HttpResponse.ok(FileType.HTML, "Hello world!")); + } + if (INDEX_PAGE.equals(url)) { + buildHtmlResponse(outputStream, url); + } + if ("/css/styles.css".equals(url)) { + buildStyleSheetResponse(outputStream, url); + } + if ("/js/scripts.js".equals(url)) { + buildScriptResponse(outputStream, url); + } + if (url.matches("/assets/.*\\.js")) { + buildScriptResponse(outputStream, url); + } + if ("/login".equals(url) && "GET".equals(method) && !isLogin(request, outputStream)) { + buildHtmlResponse(outputStream, url); + } + if ("/login".equals(url) && "POST".equals(method)) { + login(outputStream, request); + } + if ("/register".equals(url) && "GET".equals(method)) { + buildHtmlResponse(outputStream, url); + } + if ("/register".equals(url) && "POST".equals(method)) { + register(outputStream, request); + } + + outputStream.flush(); + } catch (IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + } + } - 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); + private boolean isLogin(final HttpRequest request, final OutputStream outputStream) throws IOException { + final var cookies = request.getCookies(); + return cookies.contains(JSESSIONID) && hasSession(outputStream, cookies); + } - outputStream.write(response.getBytes()); + private boolean hasSession(final OutputStream outputStream, final HttpCookie cookies) throws IOException { + final var sessionId = cookies.getCookieValue(JSESSIONID); + if (SessionManager.findSession(sessionId) != null) { + outputStream.write(HttpResponse.found(INDEX_PAGE)); outputStream.flush(); + return true; + } + return false; + } + + private void login(final OutputStream outputStream, final HttpRequest request) { + Map params = request.parseRequestQuery(); + + InMemoryUserRepository.findByAccount(params.get("account")).ifPresentOrElse(user -> { + if (user.checkPassword(params.get("password"))) { + Session session = createSession(user); + createCookie(outputStream, session); + log.info("user: {}", user); + return; + } + buildHtmlResponse(outputStream, ACCESS_DENIED_PAGE); + }, () -> buildHtmlResponse(outputStream, ACCESS_DENIED_PAGE)); + } + + private Session createSession(final User user) { + final UUID id = UUID.randomUUID(); + final Session session = new Session(id.toString()); + session.setAttribute("user", user); + SessionManager.add(session); + + return session; + } + + private static void createCookie(final OutputStream outputStream, final Session session) { + try { + HttpCookie cookie = new HttpCookie("JSESSIONID=" + session.getId()); + log.info("cookie: {}", cookie.getCookieValue(JSESSIONID)); + outputStream.write(HttpResponse.found(INDEX_PAGE, cookie)); + } catch (IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + } + } + + private void register(final OutputStream outputStream, final HttpRequest request) throws IOException { + final Map userInfos = request.parseRequestQuery(); + + final User user = new User(userInfos.get("account"), userInfos.get("password"), userInfos.get("email")); + InMemoryUserRepository.save(user); + + outputStream.write(HttpResponse.found(INDEX_PAGE)); + } + + private String buildResponseBodyFromStaticFile(final String filePath) throws IOException { + final var resourceName = STATIC_PATH + filePath; + final var path = Path.of(this.getClass().getClassLoader().getResource(resourceName).getPath()); + + return String.join("\n", Files.readAllLines(path)) + "\n"; + } + + private void buildHtmlResponse(final OutputStream outputStream, final String filePath) { + try { + if (filePath.endsWith(HTML_SUFFIX)) { + final var responseBody = buildResponseBodyFromStaticFile(filePath); + outputStream.write(HttpResponse.ok(FileType.HTML, responseBody)); + return; + } + final var responseBody = buildResponseBodyFromStaticFile(filePath + HTML_SUFFIX); + outputStream.write(HttpResponse.ok(FileType.HTML, responseBody)); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private void buildStyleSheetResponse(final OutputStream outputStream, final String fileName) throws IOException { + final var responseBody = buildResponseBodyFromStaticFile(fileName); + + outputStream.write(HttpResponse.ok(FileType.CSS, responseBody)); + } + + private void buildScriptResponse(final OutputStream outputStream, final String fileName) throws IOException { + final var responseBody = buildResponseBodyFromStaticFile(fileName); + + outputStream.write(HttpResponse.ok(FileType.JAVASCRIPT, responseBody)); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java new file mode 100644 index 0000000000..77d544d8ba --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpCookie { + private final Map cookies; + + public HttpCookie(final String values) { + this.cookies = Arrays.stream(values.split("; ")) + .map(token -> token.split("=")) + .collect(Collectors.toMap(value -> value[0], value -> value[1])); + } + + public boolean contains(final String key) { + return cookies.containsKey(key); + } + + public String getCookieValue(final String key) { + return cookies.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..07ca14b45a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,81 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.coyote.http11.HttpCookie; + +public class HttpRequest { + private final RequestLine requestLine; + private final Map headers; + private final String requestBody; + + private HttpRequest(final RequestLine requestLine, final Map headers, final String requestBody) { + this.requestLine = requestLine; + this.headers = headers; + this.requestBody = requestBody; + } + + public static HttpRequest from(final BufferedReader bufferedReader) throws IOException { + final RequestLine requestLine = parseRequestLine(bufferedReader); + final Map headers = parseHeaders(bufferedReader); + final String body = parseBody(bufferedReader, Integer.parseInt(headers.getOrDefault("Content-Length", "0"))); + return new HttpRequest(requestLine, headers, body); + } + + private static RequestLine parseRequestLine(final BufferedReader bufferedReader) throws IOException { + return RequestLine.of(bufferedReader.readLine()); + } + + private static Map parseHeaders(final BufferedReader reader) throws IOException { + final Map headers = new HashMap<>(); + var header = reader.readLine(); + + while (!"".equals(header)) { + final var tokens = header.split(": "); + headers.put(tokens[0], tokens[1]); + header = reader.readLine(); + } + + return headers; + } + + private static String parseBody(final BufferedReader reader, final int contentLength) throws IOException { + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + return new String(buffer); + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public String getRequestMethod() { + return requestLine.getMethod(); + } + + public String getRequestPath() { + return requestLine.getPath(); + } + + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } + + public HttpCookie getCookies() { + final String cookieString = headers.getOrDefault("Cookie", ""); + return new HttpCookie(cookieString); + } + + public Map parseRequestQuery() { + final var params = requestBody.split("&"); + + return Arrays.stream(params) + .map(param -> param.split("=")) + .collect(Collectors.toMap(token -> token[0], token -> token[1])); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java new file mode 100644 index 0000000000..98065e49a2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.request; + +public class RequestLine { + public static final String DELIMITER = " "; + + private final String method; + private final String path; + + private RequestLine(final String method, final String path) { + this.method = method; + this.path = path; + } + + public static RequestLine of(final String requestLine) { + final var method = requestLine.split(DELIMITER)[0]; + final var path = requestLine.split(DELIMITER)[1]; + return new RequestLine(method, path); + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..3979e2b1f6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,54 @@ +package org.apache.coyote.http11.response; + +import org.apache.coyote.http11.FileType; +import org.apache.coyote.http11.HttpCookie; + +public class HttpResponse { + private static final String DELIMITER = "\r\n"; + private static final String OK_STATUS_LINE = "HTTP/1.1 200 OK "; + private static final String FOUND_STATUS_LINE = "HTTP/1.1 302 Found "; + public static final String EMPTY_LINE = ""; + + public static byte[] ok(final FileType fileType, final String responseBody) { + return String.join(DELIMITER, OK_STATUS_LINE, + setContentTypeHeader(fileType), + setContentLengthHeader(responseBody), + EMPTY_LINE, + responseBody + ).getBytes(); + } + + public static byte[] found(final String location) { + return String.join(DELIMITER, FOUND_STATUS_LINE, + setLocationHeader(location), + EMPTY_LINE + ).getBytes(); + } + + public static byte[] found(final String location, final HttpCookie cookie) { + return String.join(DELIMITER, FOUND_STATUS_LINE, + setCookieHeader(cookie), + setLocationHeader(location), + EMPTY_LINE + ).getBytes(); + } + + private static String setContentTypeHeader(final FileType fileType) { + return "Content-Type: " + fileType.getValue() + ";charset=utf-8 "; + } + + private static String setContentLengthHeader(final String responseBody) { + return "Content-Length: " + responseBody.getBytes().length + " "; + } + + private static String setLocationHeader(final String location) { + return "Location: " + location + " "; + } + + private static String setCookieHeader(final HttpCookie cookie) { + return "Set-Cookie: JSESSIONID=" + cookie.getCookieValue("JSESSIONID") + " "; + } + + private HttpResponse() { + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..1bf3bff134 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,70 @@ - - - - - - - 로그인 - - - - -

-
-
-
-
-
-
-

로그인

-
-
-
- - -
-
- - -
-
- -
-
+ + + + + + + 로그인 + + + + +
+
+
+
+
+
+
+

로그인

+
+
+
+ +
- +
+ +
+
+
+
-
+
-
+
+
- - - - + + + + + +