diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..c58b49b552 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,61 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; -import java.nio.file.Path; -import java.util.Collections; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; 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 URL url = this.getClass().getClassLoader().getResource("nextstep.txt"); final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final String actual = url.getPath(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { - final String fileName = "nextstep.txt"; - - // todo - final Path path = null; - - // todo - final List actual = Collections.emptyList(); + void 파일의_내용을_읽는다() throws URISyntaxException, IOException { + final URL url = this.getClass().getClassLoader().getResource("nextstep.txt"); + final File file = new File(url.toURI()); + + final List actual = new ArrayList<>(); + try ( + final FileInputStream fileInputStream = new FileInputStream(file); + final BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) + ) { + actual.add(bufferedReader.readLine()); + } assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..e29d65f4aa 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,5 +1,6 @@ package study; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -24,6 +25,8 @@ @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { + public static final byte[] BYTES = new byte[]{110, 101, 120, 116, 115, 116, 101, 112}; + /** * OutputStream 학습하기 * @@ -46,13 +49,9 @@ class OutputStream_학습_테스트 { */ @Test void OutputStream은_데이터를_바이트로_처리한다() throws IOException { - final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; - final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + final OutputStream outputStream = new ByteArrayOutputStream(BYTES.length); - /** - * todo - * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 - */ + outputStream.write(BYTES); final String actual = outputStream.toString(); @@ -73,11 +72,8 @@ class OutputStream_학습_테스트 { void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { final OutputStream outputStream = mock(BufferedOutputStream.class); - /** - * todo - * flush를 사용해서 테스트를 통과시킨다. - * ByteArrayOutputStream과 어떤 차이가 있을까? - */ + outputStream.write(BYTES); + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); @@ -96,6 +92,9 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try(Writer writer = new OutputStreamWriter(outputStream)) { + + } verify(outputStream, atLeastOnce()).close(); } @@ -124,13 +123,10 @@ class InputStream_학습_테스트 { byte[] bytes = {-16, -97, -92, -87}; final InputStream inputStream = new ByteArrayInputStream(bytes); - /** - * todo - * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? - */ - final String actual = ""; + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); - assertThat(actual).isEqualTo("🤩"); + assertThat(bufferedReader.readLine()).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); inputStream.close(); } @@ -148,6 +144,9 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (Reader reader = new BufferedReader(new InputStreamReader(inputStream))) { + + } verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +168,13 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream, 4); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); + // 기본 버퍼의 크기는 8192Byte(8KB) assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,7 +197,7 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", @@ -205,7 +205,14 @@ class InputStreamReader_학습_테스트 { ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + final StringBuilder actual = new StringBuilder(); + try(final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + while (bufferedReader.ready()) { + actual.append(bufferedReader.readLine()); + actual.append("\r\n"); + } + } assertThat(actual).hasToString(emoji); } 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..7754d4f4fe 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,19 +1,35 @@ package org.apache.coyote.http11; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.List; import nextstep.jwp.exception.UncheckedServletException; import org.apache.coyote.Processor; +import org.apache.coyote.http11.handler.*; +import org.apache.coyote.http11.request.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); private final Socket connection; + private final List requestHandlers = List.of( + new BasicURIHandler(), + new IndexPageHandler(), + new IndexCSSHandler(), + new HttpJavascriptHandler(), + new HttpAssetHandler(), + new LoginPageHandler(), + new LoginHandler(), + new RegistrationPageHandler(), + new RegistrationHandler() + ); + public Http11Processor(final Socket connection) { this.connection = connection; } @@ -26,20 +42,16 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - - final var responseBody = "Hello world!"; - - 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); - - outputStream.write(response.getBytes()); - outputStream.flush(); + try ( + final InputStream inputStream = connection.getInputStream(); + final OutputStream outputStream = connection.getOutputStream() + ) { + final HttpRequest httpRequest = HttpRequest.from(inputStream); + for (HttpRequestHandler requestHandler : this.requestHandlers) { + if (requestHandler.support(httpRequest)) { + requestHandler.handle(httpRequest, outputStream); + } + } } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java new file mode 100644 index 0000000000..288d1f4389 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/BasicURIHandler.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.handler; + +import java.io.IOException; +import java.io.OutputStream; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public class BasicURIHandler implements HttpRequestHandler { + @Override + public boolean support(final HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/"); + } + + @Override + public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException { + final var responseBody = "Hello world!"; + + final HttpResponse httpResponse = new HttpResponse.Builder() + .responseBody(responseBody) + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java new file mode 100644 index 0000000000..01aa1da04d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpAssetHandler.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.handler; + +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; + +public class HttpAssetHandler implements HttpRequestHandler { + + public static final String ASSETS_PATH_PREFIX = "static/assets/"; + + @Override + public boolean support(final HttpRequest httpRequest) { + return httpRequest.isAssetRequest(); + } + + @Override + public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .contentType("text/javascript") + .responseBody(new FileHandler().readFromResourcePath(ASSETS_PATH_PREFIX + httpRequest.getEndPoint())) + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java new file mode 100644 index 0000000000..41013cb335 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpJavascriptHandler.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.handler; + +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; + +public class HttpJavascriptHandler implements HttpRequestHandler { + + public static final String JAVASCRIPT_PATH_PREFIX = "static/js/"; + + @Override + public boolean support(final HttpRequest httpRequest) { + return httpRequest.isJavascriptRequest(); + } + + @Override + public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .contentType("text/javascript") + .responseBody(new FileHandler().readFromResourcePath(JAVASCRIPT_PATH_PREFIX + httpRequest.getEndPoint())) + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java new file mode 100644 index 0000000000..24bf10e4e0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HttpRequestHandler.java @@ -0,0 +1,11 @@ +package org.apache.coyote.http11.handler; + +import java.io.IOException; +import java.io.OutputStream; +import org.apache.coyote.http11.request.HttpRequest; + +public interface HttpRequestHandler { + boolean support(HttpRequest httpRequest); + + void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException; +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java new file mode 100644 index 0000000000..4c323ba7c8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexCSSHandler.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.handler; + +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; + +public class IndexCSSHandler implements HttpRequestHandler { + + public static final String CSS_PATH_PREFIX = "static/css/"; + + @Override + public boolean support(final HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/css/styles.css"); + } + + @Override + public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .contentType("text/css") + .responseBody(new FileHandler().readFromResourcePath(CSS_PATH_PREFIX + httpRequest.getEndPoint())) + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java new file mode 100644 index 0000000000..2828c0b3b6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/IndexPageHandler.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.handler; + +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; + +public class IndexPageHandler implements HttpRequestHandler { + @Override + public boolean support(final HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/index.html"); + } + + @Override + public void handle(final HttpRequest httpRequest, final OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .responseBody(new FileHandler().readFromResourcePath("static/index.html")) + .build(outputStream); + + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java new file mode 100644 index 0000000000..423fcba9e8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginHandler.java @@ -0,0 +1,81 @@ +package org.apache.coyote.http11.handler; + +import java.util.Map; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.*; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.session.Session; +import org.apache.coyote.http11.session.SessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Optional; +import java.util.UUID; + +public class LoginHandler implements HttpRequestHandler { + + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + + @Override + public boolean support(HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("POST") && httpRequest.isUriEqualTo("/login"); + } + + @Override + public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException { + final Map requestBody = httpRequest.getRequestBodyAsMap(); + final Optional account = Optional.ofNullable(requestBody.get("account")); + final Optional password = Optional.ofNullable(requestBody.get("password")); + + if (account.isEmpty() || password.isEmpty()) { + returnUnauthorizedPage(outputStream); + return; + } + verifyAccount(account.get(), password.get(), outputStream); + } + + private void verifyAccount(String account, String password, OutputStream outputStream) throws IOException { + final Optional user = InMemoryUserRepository.findByAccount(account); + if (user.isEmpty() || !user.get().checkPassword(password)) { + returnUnauthorizedPage(outputStream); + return; + } + logAccount(user.get()); + redirectIndexPage(outputStream, user.get()); + } + + private void returnUnauthorizedPage(OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .responseBody(new FileHandler().readFromResourcePath("static/401.html")) + .responseStatus("401") + .build(outputStream); + httpResponse.flush(); + } + + private void redirectIndexPage(OutputStream outputStream, User user) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .redirect("/index.html") + .build(outputStream); + + final String sessionId = UUID.randomUUID().toString(); + httpResponse.addCookie("JSESSIONID", sessionId); + saveUserToSession(user, sessionId); + + httpResponse.flush(); + } + + private void saveUserToSession(User user, String sessionId) { + final Session session = new Session(sessionId); + session.setAttribute(sessionId, user); + SessionManager.add(session); + } + + private void logAccount(User user) { + log.info("user : {}", user); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java new file mode 100644 index 0000000000..bae473215b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/LoginPageHandler.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.handler; + +import nextstep.jwp.model.User; +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.session.SessionManager; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Optional; + +public class LoginPageHandler implements HttpRequestHandler { + + @Override + public boolean support(HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/login") && !httpRequest.hasQueryParameter(); + } + + @Override + public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException { + final Optional sessionId = httpRequest.getSessionId(); + if (sessionId.isPresent() && SessionManager.hasSessionWithAttributeType(sessionId.get(), User.class)) { + final HttpResponse httpResponse = new HttpResponse.Builder() + .redirect("/index.html") + .build(outputStream); + httpResponse.flush(); + } + + final HttpResponse httpResponse = new HttpResponse.Builder() + .responseBody(new FileHandler().readFromResourcePath("static/login.html")) + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java new file mode 100644 index 0000000000..3cee4493c2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationHandler.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.handler; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +public class RegistrationHandler implements HttpRequestHandler { + @Override + public boolean support(HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("POST") && httpRequest.isUriEqualTo("/register"); + } + + @Override + public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException { + final Map requestBody = httpRequest.getRequestBodyAsMap(); + final User user = new User(requestBody.get("account"), requestBody.get("password"), requestBody.get("email")); + InMemoryUserRepository.save(user); + + final HttpResponse httpResponse = new HttpResponse.Builder() + .redirect("/index.html") + .build(outputStream); + httpResponse.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java new file mode 100644 index 0000000000..962087281f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/RegistrationPageHandler.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.handler; + +import org.apache.coyote.http11.resource.FileHandler; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +import java.io.IOException; +import java.io.OutputStream; + +public class RegistrationPageHandler implements HttpRequestHandler { + @Override + public boolean support(HttpRequest httpRequest) { + return httpRequest.isMethodEqualTo("GET") && httpRequest.isUriEqualTo("/register"); + } + + @Override + public void handle(HttpRequest httpRequest, OutputStream outputStream) throws IOException { + final HttpResponse httpResponse = new HttpResponse.Builder() + .responseBody(new FileHandler().readFromResourcePath("static/register.html")) + .build(outputStream); + + httpResponse.flush(); + } +} 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..54b9e32c79 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,99 @@ +package org.apache.coyote.http11.request; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringTokenizer; +import org.apache.coyote.http11.resource.Cookies; + +public class HttpRequest { + + private String uri; + + private String method; + + private String requestBody; + + private Cookies cookies; + + public static HttpRequest from(InputStream inputStream) { + return HttpRequestParser.parseFromSocket(inputStream); + } + + public HttpRequest(String uri, String method, String requestBody, Cookies cookies) { + this.uri = uri; + this.method = method; + this.requestBody = requestBody; + this.cookies = cookies; + } + + public boolean isMethodEqualTo(final String method) { + return Objects.equals(this.method, method); + } + + public boolean isUriEqualTo(final String uri) { + final int startPointOfQueryParameter = this.uri.lastIndexOf("?"); + if (startPointOfQueryParameter != -1) { + final String uriWithoutQueryParameter = this.uri.substring(0, startPointOfQueryParameter); + return Objects.equals(uriWithoutQueryParameter, uri); + } + return Objects.equals(this.uri, uri); + } + + public boolean isJavascriptRequest() { + return isMethodEqualTo("GET") && this.uri.startsWith("/js/"); + } + + public boolean isAssetRequest() { + return isMethodEqualTo("GET") && this.uri.startsWith("/assets/"); + } + + public String getEndPoint() { + return this.uri.substring(this.uri.lastIndexOf("/") + 1, this.uri.length()); + } + + public Map getQueryParameters() { + if (!hasQueryParameter()) { + return Map.of(); + } + + final String parameters = this.uri.substring(this.uri.lastIndexOf("?") + 1, this.uri.length()); + return parseParametersIntoMap(parameters); + } + + public boolean hasQueryParameter() { + return !Objects.equals(this.uri.lastIndexOf("?"), -1); + } + + public Optional getQueryParameter(String parameter) { + return Optional.ofNullable(this.getQueryParameters().get(parameter)); + } + + public Map getRequestBodyAsMap() { + return parseParametersIntoMap(this.requestBody); + } + + private HashMap parseParametersIntoMap(String parameters) { + final HashMap queryParameters = new HashMap<>(); + final StringTokenizer stringTokenizer = new StringTokenizer(parameters, "&"); + while (stringTokenizer.hasMoreTokens()) { + final String parameter = stringTokenizer.nextToken(); + parseParameterIntoMap(queryParameters, parameter); + } + return queryParameters; + } + + private static void parseParameterIntoMap(final HashMap queryParameters, final String parameter) { + final StringTokenizer stringTokenizer = new StringTokenizer(parameter, "="); + if (!stringTokenizer.hasMoreTokens()) { + return; + } + queryParameters.put(stringTokenizer.nextToken(), stringTokenizer.nextToken()); + } + + public Optional getSessionId() { + return cookies.get("JSESSIONID"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java new file mode 100644 index 0000000000..6a27de3a86 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java @@ -0,0 +1,87 @@ +package org.apache.coyote.http11.request; + +import java.io.InputStream; +import java.util.Objects; +import nextstep.jwp.exception.UncheckedServletException; +import org.apache.coyote.http11.resource.Cookies; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +public class HttpRequestParser { + + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + + public static HttpRequest parseFromSocket(InputStream inputStream) { + try { + final var bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + + final StringTokenizer stringTokenizer = new StringTokenizer(bufferedReader.readLine()); + final String httpRequestMethod = stringTokenizer.nextToken(); + final String httpRequestUri = stringTokenizer.nextToken(); + + final Map headers = getHeaders(bufferedReader); + final Cookies cookies = getCookies(headers); + final String requestBody = getRequestBody(bufferedReader); + + return new HttpRequest(httpRequestUri, httpRequestMethod, requestBody, cookies); + } catch (IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } + } + + private static Cookies getCookies(Map headers) throws IOException { + final Cookies cookies = new Cookies(); + final String cookiesHeader = headers.get("Cookie"); + + if (Objects.isNull(cookiesHeader)) { + return cookies; + } + + final StringTokenizer stringTokenizer = new StringTokenizer(cookiesHeader, "; "); + while (stringTokenizer.hasMoreTokens()) { + final String cookie = stringTokenizer.nextToken(); + addCookie(cookies, cookie); + } + + return cookies; + } + + private static void addCookie(final Cookies cookies, final String cookieEntry) { + final StringTokenizer stringTokenizer = new StringTokenizer(cookieEntry, "="); + cookies.add( + stringTokenizer.nextToken(), + stringTokenizer.nextToken() + ); + } + + private static Map getHeaders(BufferedReader bufferedReader) throws IOException { + final Map requestHeaders = new HashMap<>(); + String header; + while ((header = bufferedReader.readLine()).length() != 0) { + final StringTokenizer stringTokenizer = new StringTokenizer(header, ": "); + requestHeaders.put( + stringTokenizer.nextToken(), + stringTokenizer.nextToken() + ); + } + return requestHeaders; + } + + private static String getRequestBody(BufferedReader bufferedReader) throws IOException { + final StringBuilder requestBody = new StringBuilder(); + while (bufferedReader.ready()) { + requestBody.append((char) bufferedReader.read()); + } + return requestBody.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java new file mode 100644 index 0000000000..1a55528e17 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookie.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.resource; + +import java.util.Objects; + +public class Cookie { + + private String key; + + private String value; + + public Cookie(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public boolean isKeyName(String key) { + return Objects.equals(this.key, key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java new file mode 100644 index 0000000000..6e938b8cdc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/Cookies.java @@ -0,0 +1,26 @@ +package org.apache.coyote.http11.resource; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +public class Cookies implements Iterable { + private List cookies = new ArrayList<>(); + + @Override + public Iterator iterator() { + return cookies.iterator(); + } + + public void add(String key, String value) { + cookies.add(new Cookie(key, value)); + } + + public Optional get(String key) { + final Optional cookie = cookies.stream() + .filter(coo -> coo.isKeyName(key)) + .findAny(); + return cookie.map(Cookie::getValue); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java b/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java new file mode 100644 index 0000000000..151cbb95c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/resource/FileHandler.java @@ -0,0 +1,33 @@ +package org.apache.coyote.http11.resource; + +import java.io.*; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class FileHandler { + public String readFromResourcePath(String path) throws IOException { + final var responseBody = new StringBuilder(); + + final URL indexPageURL = this.getClass().getClassLoader().getResource(path); + final File indexFile; + try { + indexFile = new File(indexPageURL.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + try ( + final FileInputStream fileInputStream = new FileInputStream(indexFile); + final BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) + ) { + while (bufferedReader.ready()) { + responseBody + .append(bufferedReader.readLine()) + .append(System.lineSeparator()); + } + } + + return responseBody.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java new file mode 100644 index 0000000000..69c3c45336 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponse.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.response; + +import java.io.IOException; +import java.io.OutputStream; + +public class HttpRedirectResponse extends HttpResponse { + + protected final String redirectUri; + + public HttpRedirectResponse( + final OutputStream outputStream, + final String redirectUri + ) { + super(outputStream, "302", null, null, null); + this.redirectUri = redirectUri; + } + + @Override + public void flush() throws IOException { + outputStream.write(HttpRedirectResponseParser.parseToBytes(this)); + outputStream.flush(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java new file mode 100644 index 0000000000..426dec99dd --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpRedirectResponseParser.java @@ -0,0 +1,13 @@ +package org.apache.coyote.http11.response; + +public class HttpRedirectResponseParser extends HttpResponseParser { + public static byte[] parseToBytes(final HttpRedirectResponse httpResponse) { + final StringBuilder response = new StringBuilder(); + + responseStatus(httpResponse, response); + cookies(httpResponse, response); + header("Location", httpResponse.redirectUri, response); + + return response.toString().getBytes(); + } +} 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..94b0f6fded --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,96 @@ +package org.apache.coyote.http11.response; + +import java.io.IOException; +import java.io.OutputStream; +import org.apache.coyote.http11.resource.Cookies; + +public class HttpResponse { + + protected OutputStream outputStream; + + protected String responseStatus; + + protected String contentType; + + protected String charSet; + + protected int contentLength; + + protected Cookies cookies = new Cookies(); + + protected String responseBody; + + protected HttpResponse(OutputStream outputStream, String responseStatus, String contentType, String charSet, String responseBody) { + this.outputStream = outputStream; + this.responseStatus = responseStatus; + this.contentType = contentType; + this.charSet = charSet; + this.responseBody = responseBody; + this.contentLength = this.responseBody != null ? this.responseBody.getBytes().length : 0; + } + + public void flush() throws IOException { + outputStream.write(HttpResponseParser.parseToBytes(this)); + outputStream.flush(); + } + + public void addCookie(String key, String value) { + this.cookies.add(key, value); + } + + public static class Builder { + private String responseStatus; + + private String contentType; + + private String charSet; + + private String responseBody; + + public Builder responseStatus(String responseStatus) { + this.responseStatus = responseStatus; + return this; + } + + public Builder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder charSet(String charSet) { + this.charSet = charSet; + return this; + } + + public Builder responseBody(String responseBody) { + this.responseBody = responseBody; + return this; + } + + public RedirectBuilder redirect(final String redirectUrl) { + return new RedirectBuilder(redirectUrl); + } + + public HttpResponse build(OutputStream outputStream) { + return new HttpResponse( + outputStream, + this.responseStatus == null ? "200 OK" : this.responseStatus, + this.contentType == null ? "text/html" : this.contentType, + this.charSet == null ? "utf-8" : this.charSet, + this.responseBody == null ? "" : this.responseBody + ); + } + } + + public static class RedirectBuilder { + protected RedirectBuilder(final String redirectUri) { + this.redirectUri = redirectUri; + } + + private final String redirectUri; + + public HttpResponse build(OutputStream outputStream) { + return new HttpRedirectResponse(outputStream, redirectUri); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java new file mode 100644 index 0000000000..0911de0fb1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseParser.java @@ -0,0 +1,82 @@ +package org.apache.coyote.http11.response; + +import java.util.Objects; +import org.apache.coyote.http11.resource.Cookie; + +public class HttpResponseParser { + + protected HttpResponseParser() { + } + + public static byte[] parseToBytes(final HttpResponse httpResponse) { + final StringBuilder response = new StringBuilder(); + + responseStatus(httpResponse, response); + contentType(httpResponse, response); + charset(httpResponse, response); + + cookies(httpResponse, response); + contentLength(httpResponse, response); + responseBody(httpResponse, response); + + return response.toString().getBytes(); + } + + protected static void responseBody(final HttpResponse httpResponse, final StringBuilder response) { + if (Objects.nonNull(httpResponse.responseBody)) { + response.append(httpResponse.responseBody); + } + } + + protected static void contentLength(final HttpResponse httpResponse, final StringBuilder response) { + response.append("Content-Length: ") + .append(httpResponse.contentLength) + .append(" ") + .append(System.lineSeparator()); + response.append(System.lineSeparator()); + } + + protected static void cookies(final HttpResponse httpResponse, final StringBuilder response) { + for (Cookie cookie : httpResponse.cookies) { + response.append("Set-Cookie: ") + .append(cookie.getKey()) + .append("=") + .append(cookie.getValue()) + .append(" ") + .append(System.lineSeparator()); + } + } + + protected static void responseStatus(final HttpResponse httpResponse, final StringBuilder response) { + response.append("HTTP/1.1 ") + .append(httpResponse.responseStatus) + .append(" ") + .append(System.lineSeparator()); + } + + protected static void charset(final HttpResponse httpResponse, final StringBuilder response) { + if (Objects.nonNull(httpResponse.charSet)) { + response + .append(";charset=") + .append(httpResponse.charSet) + .append(" "); + } + response.append(System.lineSeparator()); + } + + protected static void header(final String headerName, final String headerValue, final StringBuilder response) { + if (Objects.nonNull(headerName) && Objects.nonNull(headerValue)) { + response.append(headerName).append(": ") + .append(headerValue) + .append(" "); + } + response.append(System.lineSeparator()); + } + + protected static void contentType(final HttpResponse httpResponse, final StringBuilder response) { + if (Objects.nonNull(httpResponse.contentType)) { + response.append("Content-Type: ") + .append(httpResponse.contentType); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java new file mode 100644 index 0000000000..703323de93 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + + private final Map attributes = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public Object getAttribute(final String name) { + return attributes.get(name); + } + + public void setAttribute(final String name, final Object value) { + attributes.put(name, value); + } + + public void removeAttribute(final String name) { + attributes.remove(name); + } + + public boolean hasAttributeType(Class attributeType) { + return attributes.values().stream() + .anyMatch(attribute -> attribute.getClass().equals(attributeType)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java new file mode 100644 index 0000000000..8c4ab30480 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.session; + +import nextstep.jwp.model.User; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class SessionManager { + + private static Map sessions = new HashMap<>(); + + public static void add(Session session) { + sessions.put(session.getId(), session); + } + + public static Optional findSession(String id) { + return Optional.ofNullable(sessions.get(id)); + } + + public static void remove(Session session) { + sessions.remove(session.getId()); + } + + public static boolean hasSessionWithAttributeType(String sessionId, Class attributeType) { + final Optional session = findSession(sessionId); + return session.map(value -> value.hasAttributeType(attributeType)) + .orElse(false); + } +} 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/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..f4fed9488a 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 @@ -1,5 +1,6 @@ package nextstep.org.apache.coyote.http11; +import java.net.URISyntaxException; import support.StubSocket; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; @@ -23,7 +24,7 @@ void process() { processor.process(socket); // then - var expected = String.join("\r\n", + var expected = String.join(System.lineSeparator(), "HTTP/1.1 200 OK ", "Content-Type: text/html;charset=utf-8 ", "Content-Length: 12 ", @@ -34,9 +35,9 @@ void process() { } @Test - void index() throws IOException { + void index() throws IOException, URISyntaxException { // given - final String httpRequest= String.join("\r\n", + final String httpRequest = String.join(System.lineSeparator(), "GET /index.html HTTP/1.1 ", "Host: localhost:8080 ", "Connection: keep-alive ", @@ -51,11 +52,11 @@ 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"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + var expected = "HTTP/1.1 200 OK " + System.lineSeparator() + + "Content-Type: text/html;charset=utf-8 " + System.lineSeparator() + + "Content-Length: 5564 " + System.lineSeparator() + + System.lineSeparator() + + new String(Files.readAllBytes(new File(resource.toURI()).toPath())); assertThat(socket.output()).isEqualTo(expected); }