diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index 978eefdc34..b97ebb5785 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,17 +1,16 @@ package cache.com.example; +import javax.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 javax.servlet.http.HttpServletResponse; - @Controller public class GreetingController { @GetMapping("/") - public String index() { + public String index(final HttpServletResponse response) { return "index"; } @@ -20,11 +19,6 @@ public String index() { */ @GetMapping("/cache-control") public String cacheControl(final HttpServletResponse response) { - final String cacheControl = CacheControl - .noCache() - .cachePrivate() - .getHeaderValue(); - response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); return "index"; } 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..d57df1f992 100644 --- a/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java +++ b/study/src/main/java/cache/com/example/cachecontrol/CacheWebConfig.java @@ -1,6 +1,11 @@ package cache.com.example.cachecontrol; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,5 +14,17 @@ public class CacheWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(final InterceptorRegistry registry) { + registry.addInterceptor(new HandlerInterceptor() { + @Override + public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, + final Object handler) + throws Exception { + String cacheControl = CacheControl + .noCache() + .cachePrivate().getHeaderValue(); + response.addHeader(HttpHeaders.CACHE_CONTROL, cacheControl); + return HandlerInterceptor.super.preHandle(request, response, handler); + } + }); } } 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..bd674c7459 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,26 @@ package cache.com.example.etag; +import cache.com.example.version.ResourceVersion; +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; -// } + private final ResourceVersion version; + + public EtagFilterConfiguration(final ResourceVersion version) { + this.version = version; + } + + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/etag"); + filterRegistrationBean.addUrlPatterns("/resources/"+version.getVersion()+"/js/*"); + 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..ec12697de5 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.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; @@ -20,6 +22,7 @@ public CacheBustingWebConfig(ResourceVersion version) { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler(PREFIX_STATIC_RESOURCES + "/" + version.getVersion() + "/**") + .setCacheControl(CacheControl.maxAge(31536000L, TimeUnit.SECONDS).cachePublic()) .addResourceLocations("classpath:/static/"); } } diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..a162802388 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -7,3 +7,7 @@ server: max-connections: 1 threads: max: 2 + compression: + enabled: true + mime-types: text/html,text/plain,text/css,application/javascript,application/json + min-response-size: 10 diff --git a/study/src/test/java/cache/com/example/GreetingControllerTest.java b/study/src/test/java/cache/com/example/GreetingControllerTest.java index 9ce2a394f7..044601035e 100644 --- a/study/src/test/java/cache/com/example/GreetingControllerTest.java +++ b/study/src/test/java/cache/com/example/GreetingControllerTest.java @@ -51,6 +51,7 @@ void testCompression() { .expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked") .expectBody(String.class).returnResult(); + log.info(response.getResponseHeaders().toString()); log.info("response body\n{}", response.getResponseBody()); } @@ -63,7 +64,6 @@ void testETag() { .expectStatus().isOk() .expectHeader().exists(HttpHeaders.ETAG) .expectBody(String.class).returnResult(); - log.info("response body\n{}", response.getResponseBody()); } diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..a9ad3994f1 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,55 @@ 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.URI; +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 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

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

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws URISyntaxException, IOException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + URI uri = getClass().getClassLoader().getResource(fileName).toURI(); + final Path path = Path.of(uri); // 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..f580c1dd0e 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,42 @@ 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; import java.io.*; +import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.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바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -54,6 +51,7 @@ class OutputStream_학습_테스트 { * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -61,13 +59,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 +73,13 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ - + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -97,26 +91,30 @@ class OutputStream_학습_테스트 { * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + + } catch (IOException e) { + e.printStackTrace(); + } + 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 +126,8 @@ 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); @@ -136,8 +135,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -149,32 +147,37 @@ class InputStream_학습_테스트 { * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + + } catch (IOException e) { + e.printStackTrace(); + } + 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; +// private static int DEFAULT_BUFFER_SIZE = 8192; + 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,30 +185,32 @@ 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를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + Reader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + String temp; + while ((temp = bufferedReader.readLine()) != null) { + actual.append(temp+"\r\n"); + } assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/nextstep/jwp/FileIOUtils.java b/tomcat/src/main/java/nextstep/jwp/FileIOUtils.java new file mode 100644 index 0000000000..92316b57cd --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/FileIOUtils.java @@ -0,0 +1,26 @@ +package nextstep.jwp; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class FileIOUtils { + + private FileIOUtils() { + } + + public static Path getPath(String resourcePath) { + try{ + return Path.of(Thread.currentThread() + .getContextClassLoader() + .getResource(resourcePath).toURI()); + }catch (NullPointerException | URISyntaxException e){ + return null; + } + } + + public static byte[] getFileInBytes(String resourcePath) throws IOException { + return Files.readAllBytes(getPath(resourcePath)); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java new file mode 100644 index 0000000000..94fd712f86 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,18 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.HttpHeaders; +import org.apache.coyote.http11.HttpServlet; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.StatusCode; + +public class HomeController extends HttpServlet { + + @Override + public void doGet(final HttpRequest req, final HttpResponse resp) { + resp.setHttpResponseStartLine(StatusCode.OK); + byte[] body = "Hello world!".getBytes(); + resp.addHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=utf-8"); + resp.setResponseBody(body); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java new file mode 100644 index 0000000000..8e264e6226 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,55 @@ +package nextstep.jwp.controller; + +import java.io.IOException; +import java.util.Optional; +import nextstep.jwp.FileIOUtils; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.session.Session; +import org.apache.coyote.http11.HttpHeaders; +import org.apache.coyote.http11.HttpServlet; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.StatusCode; + +public class LoginController extends HttpServlet { + + private static final String PREFIX = "static"; + private static final String SUFFIX = ".html"; + private static final String JSESSIONID = "JSESSIONID"; + + @Override + public void doGet(final HttpRequest req, final HttpResponse resp) throws IOException { + if (req.getSession().containskey("user")) { + resp.setHttpResponseStartLine(StatusCode.FOUND); + resp.sendRedirect("/index.html"); + return; + } + resp.setHttpResponseStartLine(StatusCode.OK); + byte[] file = FileIOUtils.getFileInBytes(PREFIX+req.getPath()+SUFFIX); + resp.addHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=utf-8"); + resp.setResponseBody(file); + } + + @Override + public void doPost(final HttpRequest req, final HttpResponse resp) { + RequestParam requestParam = RequestParam.of(req.getRequestBody()); + + Optional findAccount = InMemoryUserRepository.findByAccount(requestParam.get("account")); + + if (findAccount.isPresent()) { + User user = findAccount.get(); + if (user.checkPassword(requestParam.get("password"))) { + Session session = req.getSession(); + session.setAttribute("user", user); + resp.addCookie(JSESSIONID, req.getSession().getId()); + resp.addCookie("hello", req.getSession().getId()); + resp.addCookie("bye", req.getSession().getId()); + resp.addCookie("good", req.getSession().getId()); + resp.sendRedirect("/index.html"); + return; + } + } + resp.sendRedirect("/401.html"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java new file mode 100644 index 0000000000..542e74f7f7 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,48 @@ +package nextstep.jwp.controller; + +import java.io.IOException; +import java.util.Optional; +import nextstep.jwp.FileIOUtils; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.session.Session; +import org.apache.coyote.http11.HttpHeaders; +import org.apache.coyote.http11.HttpServlet; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.StatusCode; + +public class RegisterController extends HttpServlet { + + private static final String PREFIX = "static"; + private static final String SUFFIX = ".html"; + + @Override + public void doGet(final HttpRequest req, final HttpResponse resp) throws IOException { + byte[] file = FileIOUtils.getFileInBytes(PREFIX + req.getPath() + SUFFIX); + resp.setHttpResponseStartLine(StatusCode.OK); + resp.addHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=utf-8"); + resp.setResponseBody(file); + } + + @Override + public void doPost(final HttpRequest req, final HttpResponse resp) { + RequestParam requestParam = RequestParam.of(req.getRequestBody()); + Optional findAccount = InMemoryUserRepository.findByAccount(requestParam.get("account")); + + if (findAccount.isPresent()) { + resp.sendRedirect("/401.html"); + return; + } + User user = new User( + requestParam.get("account"), + requestParam.get("password"), + requestParam.get("email") + ); + InMemoryUserRepository.save(user); + Session session = req.getSession(); + session.setAttribute("user", user); + + resp.sendRedirect("/index.html"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RequestParam.java b/tomcat/src/main/java/nextstep/jwp/controller/RequestParam.java new file mode 100644 index 0000000000..74ffa3afd7 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RequestParam.java @@ -0,0 +1,38 @@ +package nextstep.jwp.controller; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class RequestParam { + + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final String KEY_VALUE_DELIMITER = "="; + private static final String QUERY_STRING_DELIMITER = "&"; + + private final Map params; + + private RequestParam(final Map params) { + this.params = params; + } + + public static RequestParam of(String queryString) { + Map params = new HashMap<>(); + String decodeQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8); + String[] pairs = decodeQueryString.split(QUERY_STRING_DELIMITER); + for (String pair : pairs) { + String[] query = pair.split(KEY_VALUE_DELIMITER); + params.put( + query[KEY_INDEX], + query[VALUE_INDEX] + ); + } + return new RequestParam(params); + } + + public String get(String key) { + return params.get(key); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java b/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java new file mode 100644 index 0000000000..453cc5a293 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java @@ -0,0 +1,32 @@ +package nextstep.jwp.controller; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import nextstep.jwp.FileIOUtils; +import org.apache.coyote.http11.HttpHeaders; +import org.apache.coyote.http11.HttpServlet; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.StatusCode; + +// 스프링 에러 처리는 Redirect를 사용하는 것이 아니라 RequestDispatcher를 통해 처리된다 +public class ResourceController extends HttpServlet { + + private static final String PREFIX = "static"; + + @Override + public void doGet(final HttpRequest req, final HttpResponse resp) throws IOException { + resp.setHttpResponseStartLine(StatusCode.OK); + + Path path = FileIOUtils.getPath(PREFIX + req.getPath()); + + if (path == null || !path.toFile().isFile()) { + resp.sendRedirect("/401.html"); + return; + } + + resp.addHeader(HttpHeaders.CONTENT_TYPE, Files.probeContentType(path) + "; charset=utf-8"); + resp.setResponseBody(Files.readAllBytes(path)); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..b8ad7fd38d 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -1,10 +1,9 @@ package nextstep.jwp.db; -import nextstep.jwp.model.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import nextstep.jwp.model.User; public class InMemoryUserRepository { diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..d171bb84a8 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,13 +1,12 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Connector implements Runnable { diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/session/Manager.java similarity index 52% rename from tomcat/src/main/java/org/apache/catalina/Manager.java rename to tomcat/src/main/java/org/apache/catalina/session/Manager.java index e69410f6a9..c4d07df3fa 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/session/Manager.java @@ -1,18 +1,14 @@ -package org.apache.catalina; - -import jakarta.servlet.http.HttpSession; +package org.apache.catalina.session; import java.io.IOException; /** - * A Manager manages the pool of Sessions that are associated with a - * particular Container. Different Manager implementations may support - * value-added features such as the persistent storage of session data, - * as well as migrating sessions for distributable web applications. + * A Manager manages the pool of Sessions that are associated with a particular Container. Different Manager + * implementations may support value-added features such as the persistent storage of session data, as well as migrating + * sessions for distributable web applications. *

- * In order for a Manager implementation to successfully operate - * with a Context implementation that implements reloading, it - * must obey the following constraints: + * In order for a Manager implementation to successfully operate with a Context implementation + * that implements reloading, it must obey the following constraints: *

    *
  • Must implement Lifecycle so that the Context can indicate * that a restart is required. @@ -29,28 +25,23 @@ public interface Manager { * * @param session Session to be added */ - void add(HttpSession session); + void add(Session session); /** - * Return the active Session, associated with this Manager, with the - * specified session id (if any); otherwise return null. + * Return the active Session, associated with this Manager, with the specified session id (if any); otherwise return + * null. * * @param id The session id for the session to be returned - * - * @exception IllegalStateException if a new session cannot be - * instantiated for any reason - * @exception IOException if an input/output error occurs while - * processing this request - * - * @return the request session or {@code null} if a session with the - * requested ID could not be found + * @return the request session or {@code null} if a session with the requested ID could not be found + * @throws IllegalStateException if a new session cannot be instantiated for any reason + * @throws IOException if an input/output error occurs while processing this request */ - HttpSession findSession(String id) throws IOException; + Session findSession(String id) throws IOException; /** * Remove this Session from the active Sessions for this Manager. * * @param session Session to be removed */ - void remove(HttpSession session); + void remove(Session session); } 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..27a14b35c1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,45 @@ +package org.apache.catalina.session; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashSet; +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 Enumeration getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<>(values.keySet())); + } + + 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(); + } + + public boolean containskey(String key){ + return values.containsKey(key); + } +} 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..c12ee74107 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,27 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager implements Manager { + + private static final Map SESSIONS = new HashMap<>(); + + public SessionManager() { + } + + @Override + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + @Override + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + @Override + public void remove(final Session session) { + SESSIONS.remove(session.getId()); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java index 205159e95b..390589fad4 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,11 +1,10 @@ package org.apache.catalina.startup; +import java.io.IOException; import org.apache.catalina.connector.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); 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 7f1b2c7e96..3f2711b8bc 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,13 +1,14 @@ package org.apache.coyote.http11; +import java.io.IOException; +import java.net.Socket; import nextstep.jwp.exception.UncheckedServletException; 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 { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); @@ -29,16 +30,14 @@ public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + HttpRequest httpRequest = HttpRequest.of(inputStream); + HttpResponse httpResponse = new HttpResponse(); - 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); + HttpServletMapper httpServletMapper = new HttpServletMapper(); + HttpServlet httpServlet = httpServletMapper.get(httpRequest.getPath()); + httpServlet.service(httpRequest, httpResponse); - outputStream.write(response.getBytes()); + outputStream.write(httpResponse.generateResponse()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java new file mode 100644 index 0000000000..c03a2bd939 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpHeaders.java @@ -0,0 +1,13 @@ +package org.apache.coyote.http11; + +public final class HttpHeaders { + + public static final String COOKIE = "Cookie"; + public static final String SET_COOKIE = "Set-Cookie"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String LOCATION = "Location"; + public static final String CONTENT_LENGTH = "Content-Length"; + + private HttpHeaders() { + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpServlet.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpServlet.java new file mode 100644 index 0000000000..515f2c3066 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpServlet.java @@ -0,0 +1,26 @@ +package org.apache.coyote.http11; + +import java.io.IOException; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public abstract class HttpServlet { + + protected void service(final HttpRequest req, final HttpResponse res) throws IOException { + if (req.isSameMethod(HttpMethod.GET)) { + doGet(req, res); + } + if (req.isSameMethod(HttpMethod.POST)) { + doPost(req, res); + } + } + + public void doGet(final HttpRequest req, final HttpResponse resp) throws IOException { + throw new UnsupportedOperationException(); + } + + public void doPost(final HttpRequest req, final HttpResponse resp) { + throw new UnsupportedOperationException(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpServletMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpServletMapper.java new file mode 100644 index 0000000000..a741b17a94 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpServletMapper.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11; + +import java.util.Map; +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; +import nextstep.jwp.controller.ResourceController; + +public class HttpServletMapper { + + private static final Map servletMap = Map.of( + "/", new HomeController(), + "/login", new LoginController(), + "/register", new RegisterController() + ); + private static final HttpServlet DEFAULT_SERVLET = new ResourceController(); + + public HttpServlet get(final String path) { + return servletMap.getOrDefault(path, DEFAULT_SERVLET); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/HttpMessageNotReadableException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/HttpMessageNotReadableException.java new file mode 100644 index 0000000000..b1a6cce5c8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/HttpMessageNotReadableException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class HttpMessageNotReadableException extends RuntimeException { + + public HttpMessageNotReadableException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..e916f0f0b0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,13 @@ +package org.apache.coyote.http11.request; + +public enum HttpMethod { + + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE +} 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..69bcdec7c4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,93 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.UUID; +import org.apache.catalina.session.Manager; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.http11.HttpHeaders; + +public class HttpRequest { + + private static final String EMPTY_BODY = ""; + private final HttpRequestStartLine httpRequestStartLine; + private final HttpRequestHeaders httpRequestHeaders; + private final HttpRequestCookies httpRequestCookies; + private final String requestBody; + private final Session session; + + private HttpRequest( + final HttpRequestStartLine requestLine, + final HttpRequestHeaders httpRequestHeaders, + final String requestBody + ) throws IOException { + this.httpRequestStartLine = requestLine; + this.httpRequestHeaders = httpRequestHeaders; + this.httpRequestCookies = generateHttpCookies(httpRequestHeaders); + this.requestBody = requestBody; + this.session = generateSession(); + } + + private Session generateSession() throws IOException { + Manager manager = new SessionManager(); + String jsessionId = httpRequestCookies.get("JSESSIONID"); + if (jsessionId != null && manager.findSession(jsessionId) != null) { + return manager.findSession(jsessionId); + } + Session newSession = new Session(String.valueOf(UUID.randomUUID())); + manager.add(newSession); + return newSession; + } + + private HttpRequestCookies generateHttpCookies(final HttpRequestHeaders httpRequestHeaders) { + String cookies = httpRequestHeaders.getValue(HttpHeaders.COOKIE); + if (cookies != null) { + return HttpRequestCookies.of(cookies); + } + return HttpRequestCookies.empty(); + } + + public static HttpRequest of(InputStream inputStream) throws IOException { + Reader inputStreamReader = new InputStreamReader(inputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + HttpRequestStartLine requestLine = HttpRequestStartLine.of(bufferedReader.readLine()); + HttpRequestHeaders httpRequestHeaders = HttpRequestHeaders.of(bufferedReader); + + String findContentLength = httpRequestHeaders.getValue(HttpHeaders.CONTENT_LENGTH); + String requestBody = readRequestBody(findContentLength, bufferedReader); + + return new HttpRequest(requestLine, httpRequestHeaders, requestBody); + } + + private static String readRequestBody(final String findContentLength, final BufferedReader bufferedReader) + throws IOException { + if (findContentLength != null) { + int contentLength = Integer.parseInt(findContentLength); + char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + return new String(buffer); + } + return EMPTY_BODY; + } + + public String getRequestBody() { + return requestBody; + } + + public String getPath() { + return httpRequestStartLine.getUri().getPath(); + } + + public Session getSession() { + return session; + } + + public boolean isSameMethod(HttpMethod method) { + return httpRequestStartLine.getHttpMethod()==method; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestCookies.java new file mode 100644 index 0000000000..2fb6f70794 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestCookies.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class HttpRequestCookies { + + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + private static final String KEY_VALUE_DELIMITER = "="; + private static final String COOKIE_DELIMITER = "; "; + + private final Map cookies; + + private HttpRequestCookies(final Map cookies) { + this.cookies = cookies; + } + + public static HttpRequestCookies empty() { + return new HttpRequestCookies(new HashMap<>()); + } + + public static HttpRequestCookies of(final String cookies) { + Map httpCookies = new HashMap<>(); + String[] pairs = cookies.split(COOKIE_DELIMITER); + for (String pair : pairs) { + String[] cookie = pair.split(KEY_VALUE_DELIMITER); + httpCookies.put(cookie[KEY_INDEX].strip(), cookie[VALUE_INDEX]); + } + return new HttpRequestCookies(httpCookies); + } + + public String get(final String key) { + return cookies.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java new file mode 100644 index 0000000000..4d7021cd29 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java @@ -0,0 +1,44 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.coyote.http11.exception.HttpMessageNotReadableException; + +public class HttpRequestHeaders { + + private static final int REQUEST_HEADER_KEY_INDEX = 0; + private static final int REQUEST_HEADER_VALUE_INDEX = 1; + private static final String HEADER_DELIMITER = ":"; + + private final Map handlerMap; + + private HttpRequestHeaders(final Map handlerMap) { + this.handlerMap = handlerMap; + } + + public static HttpRequestHeaders of(final BufferedReader bufferedReader) throws IOException { + final Map requestHeaders = new LinkedHashMap<>(); + + String line; + while ((line = bufferedReader.readLine()) != null && !line.isBlank()) { + String[] requestHeaderParts = line.split(HEADER_DELIMITER); + + if (requestHeaderParts.length < 2) { + throw new HttpMessageNotReadableException("잘못된 HTTP 헤더 형식"); + } + + requestHeaders.put( + requestHeaderParts[REQUEST_HEADER_KEY_INDEX].strip().toLowerCase(), + requestHeaderParts[REQUEST_HEADER_VALUE_INDEX].strip() + ); + } + + return new HttpRequestHeaders(requestHeaders); + } + + public String getValue(String key) { + return handlerMap.get(key.toLowerCase()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java new file mode 100644 index 0000000000..a20755ec7e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java @@ -0,0 +1,49 @@ +package org.apache.coyote.http11.request; + +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.coyote.http11.exception.HttpMessageNotReadableException; + +public class HttpRequestStartLine { + + private static final int HTTP_METHOD_INDEX = 0; + private static final int REQUEST_URI_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + private static final String REQUEST_LINE_DELIMITER = " "; + + private final HttpMethod httpMethod; + private final URI uri; + private final String httpVersion; + + private HttpRequestStartLine(final HttpMethod httpMethod, final URI uri, final String httpVersion) { + this.httpMethod = httpMethod; + this.uri = uri; + this.httpVersion = httpVersion; + } + + public static HttpRequestStartLine of(String requestLine) { + String[] requestLineParts = requestLine.split(REQUEST_LINE_DELIMITER); + + try { + return new HttpRequestStartLine( + HttpMethod.valueOf(requestLineParts[HTTP_METHOD_INDEX]), + new URI(requestLineParts[REQUEST_URI_INDEX]), + requestLineParts[HTTP_VERSION_INDEX] + ); + } catch (URISyntaxException e) { + throw new HttpMessageNotReadableException("HTTP StartLine URI 형식이 잘못되었습니다."); + } + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public URI getUri() { + return uri; + } + + public String getHttpVersion() { + return httpVersion; + } +} 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..068cd0965d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,76 @@ +package org.apache.coyote.http11.response; + +import java.util.stream.Collectors; +import org.apache.coyote.http11.HttpHeaders; + +public class HttpResponse { + + private static final String EMPTY_LINE = ""; + private static final String NEW_LINE = "\r\n"; + private static final String COOKIES_DELIMITER = "; "; + private final HttpResponseCookies cookies; + private final HttpResponseHeaders headers; + private HttpResponseStartLine httpResponseStartLine; + private String responseBody; + + public HttpResponse() { + this.cookies = HttpResponseCookies.empty(); + this.headers = HttpResponseHeaders.empty(); + } + + public void sendRedirect(final String location) { + httpResponseStartLine = HttpResponseStartLine.of(StatusCode.FOUND); + headers.add(HttpHeaders.LOCATION, location); + } + + public void addCookie(final String key, final String value) { + cookies.add(key, value); + } + + public void addHeader(final String name, final String value) { + headers.add(name, value); + } + + public void setHttpResponseStartLine(final StatusCode statusCode) { + httpResponseStartLine = HttpResponseStartLine.of(statusCode); + } + + public void setResponseBody(final byte[] responseBody) { + headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(responseBody.length)); + this.responseBody = new String(responseBody); + } + + public byte[] generateResponse() { + return String.join(NEW_LINE, + generateStartLine(), + generateHeaders(), + EMPTY_LINE, + responseBody).getBytes(); + } + + private String generateStartLine() { + return String.format("%s %s %s", + httpResponseStartLine.getHttpVersion(), + httpResponseStartLine.getStatusCode().getCode(), + httpResponseStartLine.getStatusCode().getText() + ); + } + + private String generateHeaders() { + String cookiesValue = generateCookies(); + if (!cookiesValue.isBlank()) { + headers.add(HttpHeaders.SET_COOKIE, cookiesValue); + } + return headers.getEntrySet() + .stream() + .map(header -> String.format("%s: %s", header.getKey(), header.getValue())) + .collect(Collectors.joining(NEW_LINE)); + } + + private String generateCookies() { + return cookies.getEntrySet() + .stream() + .map(header -> String.format("%s=%s", header.getKey(), header.getValue())) + .collect(Collectors.joining(COOKIES_DELIMITER)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseCookies.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseCookies.java new file mode 100644 index 0000000000..54e4a03ed8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseCookies.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.response; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class HttpResponseCookies { + + private final Map cookies; + + private HttpResponseCookies(final Map cookies) { + this.cookies = cookies; + } + + public static HttpResponseCookies empty() { + return new HttpResponseCookies(new HashMap<>()); + } + + public String get(final String key) { + return cookies.get(key); + } + + public String add(final String key, final String value) { + return cookies.put(key, value); + } + + public Set> getEntrySet() { + return cookies.entrySet(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java new file mode 100644 index 0000000000..f61f1174e7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseHeaders.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.response; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class HttpResponseHeaders { + + private final Map headers; + + private HttpResponseHeaders(final Map headers) { + this.headers = headers; + } + + public static HttpResponseHeaders empty() { + return new HttpResponseHeaders(new LinkedHashMap<>()); + } + + public void add(final String key, final String value) { + headers.put(key, value); + } + + public Set> getEntrySet() { + return headers.entrySet(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStartLine.java new file mode 100644 index 0000000000..bb1f6603ac --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseStartLine.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.response; + +public class HttpResponseStartLine { + + private static final String HTTP_VERSION = "HTTP/1.1"; + private final String httpVersion; + private final StatusCode statusCode; + + private HttpResponseStartLine(final String httpVersion, final StatusCode statusCode) { + this.httpVersion = httpVersion; + this.statusCode = statusCode; + } + + public static HttpResponseStartLine of(final StatusCode statusCode) { + return new HttpResponseStartLine(HTTP_VERSION, statusCode); + } + + public String getHttpVersion() { + return httpVersion; + } + + public StatusCode getStatusCode() { + return statusCode; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java new file mode 100644 index 0000000000..590b87e221 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.response; + +public enum StatusCode { + + OK("200", "OK"), + FOUND("302", "FOUND"), + BAD_REQUEST("400", "BAD_REQUEST"), + UNAUTHORIZED("401", "UNAUTHORIZED"), + NOT_FOUND("404", "NOT_FOUND"), + INTERNAL_SERVER_ERROR("500", "INTERNAL_SERVER_ERROR"); + + private final String code; + private final String text; + + StatusCode(final String code, final String text) { + this.code = code; + this.text = text; + } + + public String getCode() { + return code; + } + + public String getText() { + return text; + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

    로그인

    -
    +
    diff --git a/tomcat/src/test/java/nextstep/jwp/controller/RequestParamTest.java b/tomcat/src/test/java/nextstep/jwp/controller/RequestParamTest.java new file mode 100644 index 0000000000..bbf59f8c75 --- /dev/null +++ b/tomcat/src/test/java/nextstep/jwp/controller/RequestParamTest.java @@ -0,0 +1,23 @@ +package nextstep.jwp.controller; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestParamTest { + + @Test + void 일반_쿼리스트링_변환_테스트() { + RequestParam requestParam = RequestParam.of("nickname=파워&age=26"); + Assertions.assertThat(requestParam.get("nickname")).isEqualTo("파워"); + } + + @Test + void 인코딩된_쿼리스트링_변환_테스트() { + RequestParam requestParam = RequestParam.of("nickname%3d%ed%8c%8c%ec%9b%8c%26age%3d26"); + Assertions.assertThat(requestParam.get("nickname")).isEqualTo("파워"); + } +} diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..6f5e13a82b 100644 --- a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java +++ b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java @@ -24,9 +24,9 @@ void process() { // then var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", + "HTTP/1.1 200 OK", + "Content-Type: text/html; charset=utf-8", + "Content-Length: 12", "", "Hello world!"); @@ -36,10 +36,10 @@ void process() { @Test void index() throws IOException { // given - final String httpRequest= String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", + final String httpRequest = String.join("\r\n", + "GET /index.html HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", "", ""); @@ -51,10 +51,10 @@ void index() throws IOException { // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + - "\r\n"+ + var expected = "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Content-Length: 5564\r\n" + + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestCookiesTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestCookiesTest.java new file mode 100644 index 0000000000..8745af7b46 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestCookiesTest.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.request; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestCookiesTest { + + @Test + void request_cookies_생성_테스트() { + String jsessionId = "656cef62-e3c4-40bc-a8df-94732920ed46"; + String cookies = "Cookie: yummy_cookie=choco; tasty_cookie=strawberry; JSESSIONID=" + jsessionId; + + HttpRequestCookies httpRequestCookies = HttpRequestCookies.of(cookies); + + Assertions.assertThat(httpRequestCookies.get("JSESSIONID")).isEqualTo(jsessionId); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestHeadersTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestHeadersTest.java new file mode 100644 index 0000000000..6e8e37bc0f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestHeadersTest.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestHeadersTest { + @Test + void request_헤더_생성_테스트() throws IOException { + final String request = String.join("\r\n", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: 80", + "Content-Type: application/x-www-form-urlencoded", + ""); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(request.getBytes()); + Reader inputStreamReader = new InputStreamReader(byteArrayInputStream); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + HttpRequestHeaders httpRequestHeaders = HttpRequestHeaders.of(bufferedReader); + + assertThat(httpRequestHeaders.getValue("Content-Length")).isEqualTo("80"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java new file mode 100644 index 0000000000..86ef7d4150 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestStartLineTest.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.request; + +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestStartLineTest { + + @Test + void request_start_line_생성_테스트() { + String startLine = "GET /index.html HTTP/1.1"; + HttpRequestStartLine httpRequestStartLine = HttpRequestStartLine.of(startLine); + + assertAll( + () -> Assertions.assertThat(httpRequestStartLine.getHttpMethod()).isEqualTo(HttpMethod.GET), + () -> Assertions.assertThat(httpRequestStartLine.getHttpVersion()).isEqualTo("HTTP/1.1"), + () -> Assertions.assertThat(httpRequestStartLine.getUri().getPath()).isEqualTo("/index.html") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java new file mode 100644 index 0000000000..85ff54908d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/HttpRequestTest.java @@ -0,0 +1,52 @@ +package org.apache.coyote.http11.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestTest { + + @Test + void request_get_요청_생성_테스트() throws IOException { + final String request = String.join("\r\n", + "GET /index.html HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + "", + ""); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(request.getBytes()); + HttpRequest httpRequest = HttpRequest.of(byteArrayInputStream); + assertAll( + () -> assertThat(httpRequest.getPath()).isEqualTo("/index.html") + ); + } + + @Test + void request_post_요청_생성_테스트() throws IOException { + String body = "account=gugu&password=password&email=hkkang%40woowahan.com"; + int contentLength = body.getBytes().length; + final String request = String.join("\r\n", + "POST /register HTTP/1.1", + "Host: localhost:8080", + "Connection: keep-alive", + "Content-Length: "+contentLength, + "Content-Type: application/x-www-form-urlencoded", + "", + body); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(request.getBytes()); + HttpRequest httpRequest = HttpRequest.of(byteArrayInputStream); + assertAll( + () -> assertThat(httpRequest.getRequestBody()).isEqualTo(body), + () -> assertThat(httpRequest.getPath()).isEqualTo("/register") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java new file mode 100644 index 0000000000..c72c385532 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/HttpResponseTest.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.response; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpResponseTest { + + @Test + void httpResponse_생성_테스트() { + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setHttpResponseStartLine(StatusCode.OK); + httpResponse.addHeader("Content-Type", "text/html; charset=utf-8"); + byte[] body = "Hello World".getBytes(); + httpResponse.setResponseBody(body); + httpResponse.addHeader("Content-Length", String.valueOf(body.length)); + + var expected = "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html; charset=utf-8\r\n" + + "Content-Length: " + body.length + "\r\n" + + "\r\n" + + "Hello World"; + Assertions.assertThat(httpResponse.generateResponse()).isEqualTo(expected.getBytes()); + } +}