diff --git a/study/src/test/java/cache/com/example/GreetingControllerTest.java b/study/src/test/java/cache/com/example/GreetingControllerTest.java index 9ce2a394f7..765d4b59be 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 { @@ -28,12 +27,12 @@ class GreetingControllerTest { @Test void testNoCachePrivate() { final var response = webTestClient - .get() - .uri("/") - .exchange() - .expectStatus().isOk() - .expectHeader().cacheControl(CacheControl.noCache().cachePrivate()) - .expectBody(String.class).returnResult(); + .get() + .uri("/") + .exchange() + .expectStatus().isOk() + .expectHeader().cacheControl(CacheControl.noCache().cachePrivate()) + .expectBody(String.class).returnResult(); log.info("response body\n{}", response.getResponseBody()); } @@ -41,15 +40,15 @@ void testNoCachePrivate() { @Test void testCompression() { final var response = webTestClient - .get() - .uri("/") - .exchange() - .expectStatus().isOk() + .get() + .uri("/") + .exchange() + .expectStatus().isOk() - // gzip으로 요청 보내도 어떤 방식으로 압축할지 서버에서 결정한다. - // 웹브라우저에서 localhost:8080으로 접근하면 응답 헤더에 "Content-Encoding: gzip"이 있다. - .expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked") - .expectBody(String.class).returnResult(); + // gzip으로 요청 보내도 어떤 방식으로 압축할지 서버에서 결정한다. + // 웹브라우저에서 localhost:8080으로 접근하면 응답 헤더에 "Content-Encoding: gzip"이 있다. + .expectHeader().valueEquals(HttpHeaders.TRANSFER_ENCODING, "chunked") + .expectBody(String.class).returnResult(); log.info("response body\n{}", response.getResponseBody()); } @@ -57,46 +56,48 @@ void testCompression() { @Test void testETag() { final var response = webTestClient - .get() - .uri("/etag") - .exchange() - .expectStatus().isOk() - .expectHeader().exists(HttpHeaders.ETAG) - .expectBody(String.class).returnResult(); - + .get() + .uri("/etag") + .exchange() + .expectStatus().isOk() + .expectHeader().exists(HttpHeaders.ETAG) + .expectBody(String.class).returnResult(); + log.info("response header\n{}", response.getResponseHeaders()); log.info("response body\n{}", response.getResponseBody()); } /** * http://localhost:8080/resource-versioning - * 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. - * 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다. + * 위 url의 html 파일에서 사용하는 js, css와 같은 정적 파일에 캐싱을 적용한다. 보통 정적 파일을 캐싱 무효화하기 위해 캐싱과 함께 버전을 적용시킨다. * 정적 파일에 변경 사항이 생기면 배포할 때 버전을 바꿔주면 적용된 캐싱을 무효화(Caching Busting)할 수 있다. */ @Test void testCacheBustingOfStaticResources() { final var uri = String.format("%s/%s/js/index.js", PREFIX_STATIC_RESOURCES, version.getVersion()); + System.out.println(uri); + // "/resource-versioning/js/index.js" 경로의 정적 파일에 ETag를 사용한 캐싱이 적용되었는지 확인한다. final var response = webTestClient - .get() - .uri(uri) - .exchange() - .expectStatus().isOk() - .expectHeader().exists(HttpHeaders.ETAG) - .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()) - .expectBody(String.class).returnResult(); - + .get() + .uri(uri) + .exchange() + .expectStatus().isOk() + .expectHeader().exists(HttpHeaders.ETAG) + .expectHeader().cacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()) + .expectBody(String.class).returnResult(); + + log.info("response header\n{}", response.getResponseHeaders()); log.info("response body\n{}", response.getResponseBody()); final var etag = response.getResponseHeaders().getETag(); // 캐싱되었다면 "/resource-versioning/js/index.js"로 다시 호출했을때 HTTP status는 304를 반환한다. webTestClient.get() - .uri(uri) - .header(HttpHeaders.IF_NONE_MATCH, etag) - .exchange() - .expectStatus() - .isNotModified(); + .uri(uri) + .header(HttpHeaders.IF_NONE_MATCH, etag) + .exchange() + .expectStatus() + .isNotModified(); } } diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..8be383760f 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,11 +1,15 @@ package study; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.nio.file.Path; import java.util.Collections; import java.util.List; +import org.springframework.util.ResourceUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -24,11 +28,10 @@ class FileTest { * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? */ @Test - void resource_디렉터리에_있는_파일의_경로를_찾는다() { + void resource_디렉터리에_있는_파일의_경로를_찾는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + final String actual = ResourceUtils.getURL("classpath:" + fileName).getPath(); assertThat(actual).endsWith(fileName); } @@ -40,14 +43,12 @@ class FileTest { * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; - // todo - final Path path = null; + final Path path = ResourceUtils.getFile("classpath:" + fileName).toPath(); - // 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..63fb4d4586 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,52 @@ 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.nio.charset.StandardCharsets; +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,6 +60,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write("nextstep".getBytes()); final String actual = outputStream.toString(); @@ -61,13 +69,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 +83,14 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -96,6 +101,7 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + outputStream.close(); verify(outputStream, atLeastOnce()).close(); } @@ -103,20 +109,18 @@ class OutputStream_학습_테스트 { /** * 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 +132,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new BufferedReader(new InputStreamReader(inputStream)).readLine(); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +140,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -148,6 +151,7 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + inputStream.close(); verify(inputStream, atLeastOnce()).close(); } @@ -155,57 +159,56 @@ class InputStream_학습_테스트 { /** * 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 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? -> 8192 byte */ @Test void 필터인_BufferedInputStream를_사용해보자() { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + try (final InputStream bufferedInputStream = new BufferedInputStream(inputStream)) { + final byte[] actual = bufferedInputStream.readAllBytes(); - final byte[] actual = new byte[0]; + assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); + assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); - assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); - assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } } } /** - * 자바의 기본 문자열은 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를_사용하여_문자열을_읽어온다() { final String emoji = String.join("\r\n", - "😀😃😄😁😆😅😂🤣🥲☺️😊", - "😇🙂🙃😉😌😍🥰😘😗😙😚", - "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", - ""); + "😀😃😄😁😆😅😂🤣🥲☺️😊", + "😇🙂🙃😉😌😍🥰😘😗😙😚", + "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", + ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); final StringBuilder actual = new StringBuilder(); + new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines() + .forEach(line -> actual.append(line).append("\r\n")); assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/nextstep/filter/Interceptor.java b/tomcat/src/main/java/nextstep/filter/Interceptor.java new file mode 100644 index 0000000000..ee2d8af2bf --- /dev/null +++ b/tomcat/src/main/java/nextstep/filter/Interceptor.java @@ -0,0 +1,11 @@ +package nextstep.filter; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public interface Interceptor { + + boolean preHandle(final HttpRequest httpRequest, final HttpResponse httpResponse); + + boolean support(final HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/nextstep/filter/LoginInterceptor.java b/tomcat/src/main/java/nextstep/filter/LoginInterceptor.java new file mode 100644 index 0000000000..ad2e397a07 --- /dev/null +++ b/tomcat/src/main/java/nextstep/filter/LoginInterceptor.java @@ -0,0 +1,30 @@ +package nextstep.filter; + +import static org.apache.coyote.http11.StaticPages.INDEX_PAGE; + +import java.util.List; +import org.apache.catalina.SessionManager; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public class LoginInterceptor implements Interceptor { + + private static final List SUPPORT_PATH = List.of( + "/login" + ); + + @Override + public boolean support(final HttpRequest httpRequest) { + return SUPPORT_PATH.contains(httpRequest.getPath()); + } + + @Override + public boolean preHandle(final HttpRequest httpRequest, final HttpResponse httpResponse) { + if (httpRequest.containsCookieAndJSessionID()) { + SessionManager.getInstance().validateSession(httpRequest.getCookie()); + httpResponse.sendRedirect(INDEX_PAGE); + return false; + } + return true; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/Controller.java b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java new file mode 100644 index 0000000000..838dc12fef --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/Controller.java @@ -0,0 +1,11 @@ +package nextstep.jwp.controller; + +import java.io.IOException; +import org.apache.coyote.http11.HttpRequest; + +public interface Controller { + + ResponseEntity service(final HttpRequest httpRequest) throws IOException; + + boolean canHandle(final HttpRequest httpRequest); +} 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..111195f5bd --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,77 @@ +package nextstep.jwp.controller; + +import static org.apache.coyote.http11.StaticPages.INDEX_PAGE; +import static org.apache.coyote.http11.StaticPages.UNAUTHORIZED_PAGE; + +import java.util.Map; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.catalina.SessionManager; +import org.apache.coyote.http11.HttpCookie; +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponseStatusLine; +import org.apache.coyote.http11.ResponseEntityFactory; +import org.apache.coyote.http11.Session; + +public class LoginController extends RestController { + + private static final String SUPPORTED_CONTENT_TYPE = "application/x-www-form-urlencoded"; + + public LoginController() { + super("/login"); + } + + private static Session createSession(final User user) { + final Session session = new Session(); + SessionManager.getInstance().add(session); + session.setAttribute("user", user); + return session; + } + + @Override + public ResponseEntity service(final HttpRequest httpRequest) { + return login(httpRequest); + } + + private ResponseEntity login(final HttpRequest httpRequest) { + try { + final Map body = httpRequest.getBody(); + final String account = body.get("account"); + final String password = body.get("password"); + + final User user = findUser(account); + validatePassword(user, password); + + return makeSuccessResponse(user); + } catch (IllegalArgumentException e) { + return ResponseEntityFactory.createRedirectHttpResponse(HttpResponseStatusLine.FOUND(), UNAUTHORIZED_PAGE); + } + } + + private User findUser(final String account) { + return InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 계정입니다.")); + } + + private void validatePassword(final User user, final String password) { + if (!user.checkPassword(password)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } + } + + private ResponseEntity makeSuccessResponse(final User user) { + final Session session = createSession(user); + final HttpCookie httpCookie = HttpCookie.fromJSessionId(session.getId()); + return ResponseEntityFactory.createRedirectHttpResponse(HttpResponseStatusLine.FOUND(), INDEX_PAGE, + httpCookie); + } + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final boolean isPostMethod = httpRequest.isSameMethod(HttpMethod.POST); + final boolean isSupportedContentType = httpRequest.containsContentType(SUPPORTED_CONTENT_TYPE); + + return super.canHandle(httpRequest) && isPostMethod && isSupportedContentType; + } +} 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..1c03db0b3d --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,37 @@ +package nextstep.jwp.controller; + +import static org.apache.coyote.http11.StaticPages.INDEX_PAGE; + +import java.util.Map; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponseStatusLine; +import org.apache.coyote.http11.ResponseEntityFactory; + +public class RegisterController extends RestController { + + private static final String SUPPORTED_CONTENT_TYPE = "application/x-www-form-urlencoded"; + + public RegisterController() { + super("/register"); + } + + @Override + public ResponseEntity service(final HttpRequest httpRequest) { + final Map body = httpRequest.getBody(); + final User user = new User(body.get("account"), body.get("password"), body.get("email")); + InMemoryUserRepository.save(user); + + return ResponseEntityFactory.createRedirectHttpResponse(HttpResponseStatusLine.FOUND(), INDEX_PAGE); + } + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final boolean isPostMethod = httpRequest.isSameMethod(HttpMethod.POST); + final boolean isSupportedContentType = httpRequest.containsContentType(SUPPORTED_CONTENT_TYPE); + + return super.canHandle(httpRequest) && isPostMethod && isSupportedContentType; + } +} 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..5a6ee7709a --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/ResourceController.java @@ -0,0 +1,46 @@ +package nextstep.jwp.controller; + +import java.io.IOException; +import java.util.List; +import java.util.NoSuchElementException; +import org.apache.coyote.http11.HttpMethod; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpRequestURI; +import org.apache.coyote.http11.ResponseEntityFactory; +import org.apache.coyote.util.FileFinder; + +public class ResourceController implements Controller { + + private final List staticResourceRequest = List.of( + "/login", + "/register" + ); + + @Override + public ResponseEntity service(final HttpRequest httpRequest) throws IOException { + return ResponseEntityFactory.createStaticResourceHttpResponse(httpRequest); + } + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + return existsFilePath(httpRequest.getPath()) || isStaticResourceRequest(httpRequest); + } + + private boolean existsFilePath(final String pathString) { + try { + FileFinder.readFile(pathString); + return true; + } catch (NoSuchElementException | IOException e) { + return false; + } + } + + private boolean isStaticResourceRequest(final HttpRequest httpRequest) { + final HttpRequestURI httpRequestURI = httpRequest.getRequestURI(); + final boolean isStaticResourceRequest = staticResourceRequest.stream() + .anyMatch(httpRequestURI::hasSamePath); + final boolean isGetMethod = httpRequest.isSameMethod(HttpMethod.GET); + + return isStaticResourceRequest && isGetMethod; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java b/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java new file mode 100644 index 0000000000..71b5e42ad4 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/ResponseEntity.java @@ -0,0 +1,33 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.HttpHeaders; +import org.apache.coyote.http11.HttpResponseStatusLine; + +public class ResponseEntity { + + private final HttpResponseStatusLine responseLine; + private final HttpHeaders headers; + private final String body; + + public ResponseEntity(final HttpResponseStatusLine responseLine, final HttpHeaders headers, final String body) { + this.responseLine = responseLine; + this.headers = headers; + this.body = body; + } + + public ResponseEntity(final String contentType, final String body) { + this(HttpResponseStatusLine.OK(), HttpHeaders.makeHttpResponseHeaders(contentType, body), body); + } + + public HttpResponseStatusLine getResponseLine() { + return responseLine; + } + + public HttpHeaders getHeaders() { + return headers; + } + + public String getBody() { + return body; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RestController.java b/tomcat/src/main/java/nextstep/jwp/controller/RestController.java new file mode 100644 index 0000000000..fa2bc7db5c --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RestController.java @@ -0,0 +1,19 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpRequestURI; + +public abstract class RestController implements Controller { + + protected final String path; + + protected RestController(final String path) { + this.path = path; + } + + @Override + public boolean canHandle(final HttpRequest httpRequest) { + final HttpRequestURI httpRequestURI = httpRequest.getRequestURI(); + return httpRequestURI.hasSamePath(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..af9a72e263 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -1,27 +1,29 @@ 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 { private static final Map database = new ConcurrentHashMap<>(); + private static long AUTO_INCREMENTED_ID = 1; static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); + final User user = new User(AUTO_INCREMENTED_ID, "gugu", "password", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } + private InMemoryUserRepository() { + } + public static void save(User user) { - database.put(user.getAccount(), user); + final User userForSave = new User(++AUTO_INCREMENTED_ID, user.getAccount(), user.getPassword(), user.getEmail()); + database.put(userForSave.getAccount(), userForSave); } public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - - private InMemoryUserRepository() {} } diff --git a/tomcat/src/main/java/nextstep/jwp/exception/InvalidSessionException.java b/tomcat/src/main/java/nextstep/jwp/exception/InvalidSessionException.java new file mode 100644 index 0000000000..36c35b3e19 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/exception/InvalidSessionException.java @@ -0,0 +1,8 @@ +package nextstep.jwp.exception; + +public class InvalidSessionException extends RuntimeException { + + public InvalidSessionException() { + super("유효한 세션이 아닙니다."); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/model/User.java b/tomcat/src/main/java/nextstep/jwp/model/User.java index 4c2a2cd184..775a11adae 100644 --- a/tomcat/src/main/java/nextstep/jwp/model/User.java +++ b/tomcat/src/main/java/nextstep/jwp/model/User.java @@ -26,13 +26,25 @@ public String getAccount() { return account; } + public Long getId() { + return id; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + @Override public String toString() { return "User{" + - "id=" + id + - ", account='" + account + '\'' + - ", email='" + email + '\'' + - ", password='" + password + '\'' + - '}'; + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; } } diff --git a/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java b/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java new file mode 100644 index 0000000000..c3f8764f41 --- /dev/null +++ b/tomcat/src/main/java/nextstep/servlet/DispatcherServlet.java @@ -0,0 +1,47 @@ +package nextstep.servlet; + +import java.io.IOException; +import java.util.List; +import nextstep.filter.Interceptor; +import nextstep.filter.LoginInterceptor; +import nextstep.jwp.controller.Controller; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; +import nextstep.jwp.controller.ResourceController; +import nextstep.jwp.controller.ResponseEntity; +import org.apache.coyote.http11.HttpRequest; +import org.apache.coyote.http11.HttpResponse; + +public class DispatcherServlet { + + private final List controllers = List.of( + new LoginController(), + new RegisterController(), + new ResourceController() + ); + private final List interceptors = List.of( + new LoginInterceptor() + ); + + public void service(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException { + if (!isPassedThroughInterceptors(httpRequest, httpResponse)) { + return; + } + final Controller controller = findController(httpRequest); + final ResponseEntity responseEntity = controller.service(httpRequest); + httpResponse.addAttributes(responseEntity); + } + + private boolean isPassedThroughInterceptors(final HttpRequest httpRequest, final HttpResponse httpResponse) { + return interceptors.stream() + .filter(interceptor -> interceptor.support(httpRequest)) + .allMatch(interceptor -> interceptor.preHandle(httpRequest, httpResponse)); + } + + private Controller findController(final HttpRequest httpRequest) { + return controllers.stream() + .filter(controller -> controller.canHandle(httpRequest)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("요청을 처리할 수 없습니다.")); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..ce3561134d 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,18 +1,16 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; - import java.io.IOException; +import java.util.Optional; +import org.apache.coyote.http11.Session; /** - * 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: *