diff --git a/README.md b/README.md index b24f542e33..a6b9d0453e 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# 톰캣 구현하기 +# 톰캣 구현하기 + + +### html 파일 응답하기 +* [x] HTTP Request Header에서 필요한 정보를 파싱한다. + * [x] HTTP 메소드를 저장한다. + * [x] Request URI(html 파일)를 저장한다. + * [x] HTTP 버전을 저장한다. +* [x] Request URI에 해당하는 파일을 responseBody로 돌려준다. + +### CSS 지원하기 +* [x] 요청 리소스가 CSS or JS인 경우 Response Header에 적절한 Content-Type을 보낸다. + +### 로그인 +* [x] 로그인 페이지를 응답한다. + * [x] Request URI에서 QueryString을 파싱해 저장한다. + * [x] QueryString에서 유저의 정보를 추출해 콘솔로 출력한다. + * [x] 세션 정보가 있다면 index 페이지로 리다이렉트 한다. +* [x] 로그인에 성공하면 index 페이지로 리다이렉트한다. + * [x] Cookie에 JSESSIONID가 없다면 추가한다. +* [x] 로그인에 실패하면 401 페이지로 리다이렉트한다. + +### 회원가입 +* [x] 회원가입 페이지를 응답한다. +* [x] 회원가입을 수행하면 index 페이지로 리다이렉트한다. diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..5f6304fa40 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,51 @@ 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.File; +import java.io.IOException; +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_디렉터리에_있는_파일의_경로를_찾는다() { + void resource_디렉터리에_있는_파일의_경로를_찾는다() throws IOException { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + final URL resource = getClass().getClassLoader().getResource(fileName); + assert resource != null; + final String actual = resource.getPath(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; - - // todo - final Path path = null; - - // todo - final List actual = Collections.emptyList(); + final URL resource = getClass().getClassLoader().getResource(fileName); + final Path path = new File(resource.getPath()).toPath(); + 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..88771c0a78 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,51 @@ package study; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.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 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 +59,7 @@ class OutputStream_학습_테스트 { * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -61,13 +68,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,24 +82,26 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { final OutputStream outputStream = mock(OutputStream.class); - + final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; /** * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } 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,8 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + + final String actual = new String(inputStream.readAllBytes()); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +141,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -148,33 +152,33 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } } /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -182,30 +186,34 @@ 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()); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + String line = bufferedReader.readLine(); + while (line != null) { + actual.append(line); + actual.append("\r\n"); + line = bufferedReader.readLine(); + } assertThat(actual).hasToString(emoji); } diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..8d1f3b5391 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 { @@ -23,5 +22,14 @@ public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - private InMemoryUserRepository() {} + public static Optional findByAccountAndPassword(final String account, final String password) { + final Optional user = Optional.ofNullable(database.get(account)); + if (user.isPresent() && !user.get().checkPassword(password)) { + return Optional.empty(); + } + return user; + } + + private 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..cfd020ac8d 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,95 +1,98 @@ -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; - -public class Connector implements Runnable { - - private static final Logger log = LoggerFactory.getLogger(Connector.class); - - private static final int DEFAULT_PORT = 8080; - private static final int DEFAULT_ACCEPT_COUNT = 100; - - private final ServerSocket serverSocket; - private boolean stopped; - - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); - } - - public Connector(final int port, final int acceptCount) { - this.serverSocket = createServerSocket(port, acceptCount); - this.stopped = false; - } - - private ServerSocket createServerSocket(final int port, final int acceptCount) { - try { - final int checkedPort = checkPort(port); - final int checkedAcceptCount = checkAcceptCount(acceptCount); - return new ServerSocket(checkedPort, checkedAcceptCount); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public void start() { - var thread = new Thread(this); - thread.setDaemon(true); - thread.start(); - stopped = false; - log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); - } - - @Override - public void run() { - // 클라이언트가 연결될때까지 대기한다. - while (!stopped) { - connect(); - } - } - - private void connect() { - try { - process(serverSocket.accept()); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private void process(final Socket connection) { - if (connection == null) { - return; - } - var processor = new Http11Processor(connection); - new Thread(processor).start(); - } - - public void stop() { - stopped = true; - try { - serverSocket.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private int checkPort(final int port) { - final var MIN_PORT = 1; - final var MAX_PORT = 65535; - - if (port < MIN_PORT || MAX_PORT < port) { - return DEFAULT_PORT; - } - return port; - } - - private int checkAcceptCount(final int acceptCount) { - return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); - } -} +package org.apache.catalina.connector; + +import org.apache.coyote.http11.Http11Processor; +import org.apache.coyote.http11.handler.HandlerMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class Connector implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(Connector.class); + + private static final int DEFAULT_PORT = 8080; + private static final int DEFAULT_ACCEPT_COUNT = 100; + + private final ServerSocket serverSocket; + private final HandlerMapper handlerMapper; + private boolean stopped; + + public Connector(final HandlerMapper handlerMapper) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, handlerMapper); + } + + public Connector(final int port, final int acceptCount, final HandlerMapper handlerMapper) { + this.serverSocket = createServerSocket(port, acceptCount); + this.stopped = false; + this.handlerMapper = handlerMapper; + } + + private ServerSocket createServerSocket(final int port, final int acceptCount) { + try { + final int checkedPort = checkPort(port); + final int checkedAcceptCount = checkAcceptCount(acceptCount); + return new ServerSocket(checkedPort, checkedAcceptCount); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void start() { + var thread = new Thread(this); + thread.setDaemon(true); + thread.start(); + stopped = false; + log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); + } + + @Override + public void run() { + // 클라이언트가 연결될때까지 대기한다. + while (!stopped) { + connect(); + } + } + + private void connect() { + try { + process(serverSocket.accept()); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private void process(final Socket connection) { + if (connection == null) { + return; + } + var processor = new Http11Processor(connection, handlerMapper); + new Thread(processor).start(); + } + + public void stop() { + stopped = true; + try { + serverSocket.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private int checkPort(final int port) { + final var MIN_PORT = 1; + final var MAX_PORT = 65535; + + if (port < MIN_PORT || MAX_PORT < port) { + return DEFAULT_PORT; + } + return port; + } + + private int checkAcceptCount(final int acceptCount) { + return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); + } +} 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..4e6aeb80fe 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,27 +1,30 @@ -package org.apache.catalina.startup; - -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); - - public void start() { - var connector = new Connector(); - connector.start(); - - try { - // make the application wait until we press any key. - System.in.read(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } finally { - log.info("web server stop."); - connector.stop(); - } - } -} +package org.apache.catalina.startup; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.handler.HandlerMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class Tomcat { + + private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + + private final HandlerMapper handlerMapper = new HandlerMapper(); + + public void start() { + var connector = new Connector(handlerMapper); + connector.start(); + + try { + // make the application wait until we press any key. + System.in.read(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + log.info("web server stop."); + connector.stop(); + } + } +} 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..e2626c7b5a 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,47 +1,50 @@ -package org.apache.coyote.http11; - -import nextstep.jwp.exception.UncheckedServletException; -import org.apache.coyote.Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.Socket; - -public class Http11Processor implements Runnable, Processor { - - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - - private final Socket connection; - - public Http11Processor(final Socket connection) { - this.connection = connection; - } - - @Override - public void run() { - log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); - process(connection); - } - - @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(); - } catch (IOException | UncheckedServletException e) { - log.error(e.getMessage(), e); - } - } -} +package org.apache.coyote.http11; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import nextstep.jwp.exception.UncheckedServletException; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.handler.HandlerMapper; +import org.apache.coyote.http11.message.request.Request; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Http11Processor implements Runnable, Processor { + + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + + private final Socket connection; + + private final HandlerMapper handlerMapper; + + public Http11Processor(final Socket connection, final HandlerMapper handlerMapper) { + this.connection = connection; + this.handlerMapper = handlerMapper; + } + + @Override + public void run() { + log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + process(connection); + } + + @Override + public void process(final Socket connection) { + try (final var inputStream = connection.getInputStream(); + final var outputStream = connection.getOutputStream(); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + final Request request = Request.from(bufferedReader); + final String response = createResponse(request); + outputStream.write(response.getBytes()); + outputStream.flush(); + } catch (final IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + } + } + + private String createResponse(final Request request) { + return handlerMapper.handle(request).getResponse(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/Session.java new file mode 100644 index 0000000000..26f2ad4723 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Session.java @@ -0,0 +1,34 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + private final String id; + private final Map values = new HashMap<>(); + + public Session() { + this.id = String.valueOf(UUID.randomUUID()); + } + + public String getId() { + return this.id; + } + + public Object getAttribute(final String name) { + return values.get(name); + } + + public void setAttribute(final String name, final Object value) { + values.put(name, value); + } + + public void removeAttribute(final String name) { + values.remove(name); + } + + public void invalidate() { + values.clear(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java new file mode 100644 index 0000000000..a6840c995b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/SessionManager.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + private static final Map SESSIONS = new HashMap<>(); + + public static void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public static Session findSession(final String id) { + return SESSIONS.get(id); + } + + public static void remove(final String id) { + SESSIONS.remove(id); + } + + private SessionManager() { + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java new file mode 100644 index 0000000000..b029db6fa2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerMapper.java @@ -0,0 +1,98 @@ +package org.apache.coyote.http11.handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.Session; +import org.apache.coyote.http11.SessionManager; +import org.apache.coyote.http11.message.HttpStatus; +import org.apache.coyote.http11.message.request.Request; +import org.apache.coyote.http11.message.request.RequestLine; +import org.apache.coyote.http11.message.response.Response; + +public class HandlerMapper { + private final Map> handlers = new HashMap<>(); + + public HandlerMapper() { + init(); + } + + private void init() { + handlers.put(new HandlerStatus("GET", "/"), this::rootHandler); + handlers.put(new HandlerStatus("GET", "/login"), this::loginHandler); + handlers.put(new HandlerStatus("POST", "/login"), this::loginFormHandler); + handlers.put(new HandlerStatus("GET", "/register"), this::registerHandler); + handlers.put(new HandlerStatus("POST", "/register"), this::registerFormHandler); + } + + public Response rootHandler(final Request request) { + return Response.createByResponseBody(HttpStatus.OK, "Hello world!"); + } + + public Response loginHandler(final Request request) { + if (request.getSessionValue("user") != Optional.empty()) { + return Response.createByTemplate(HttpStatus.FOUND, "index.html"); + } + + return Response.createByTemplate(HttpStatus.OK, "login.html"); + } + + public Response loginFormHandler(final Request request) { + final Map requestForms = request.getRequestForms().getFormData(); + final Optional user = login(requestForms.get("account"), requestForms.get("password")); + return user.map(value -> loginSuccess(request, value)).orElseGet(this::loginFail); + } + + private Response loginSuccess(final Request request, final User user) { + if (request.noSession()) { + final Session session = new Session(); + session.setAttribute("user", user); + SessionManager.add(session); + + final Map header = new HashMap<>(); + header.put("Set-Cookie", "JSESSIONID=" + session.getId()); + return Response.createByTemplate(HttpStatus.FOUND, "index.html", header); + } + return Response.createByTemplate(HttpStatus.FOUND, "index.html"); + } + + private Response loginFail() { + return Response.createByTemplate(HttpStatus.UNAUTHORIZED, "401.html"); + } + + private Optional login(final String account, final String password) { + return InMemoryUserRepository.findByAccountAndPassword(account, password); + } + + public Response registerHandler(final Request request) { + return Response.createByTemplate(HttpStatus.OK, "register.html"); + } + + public Response registerFormHandler(final Request request) { + final Map requestForms = request.getRequestForms().getFormData(); + final String account = requestForms.get("account"); + final String email = requestForms.get("email"); + final String password = requestForms.get("password"); + InMemoryUserRepository.save(new User(account, password, email)); + + return Response.createByTemplate(HttpStatus.FOUND, "index.html"); + } + + public Response handle(final Request request) { + final RequestLine requestLine = request.getRequestLine(); + final HandlerStatus handlerStatus = HandlerStatus.from(requestLine); + + final Function handler = handlers.get(handlerStatus); + if (handler != null) { + return handler.apply(request); + } + + if (requestLine.getRequestURI().isExistFile()) { + return Response.createByTemplate(request.getRequestLine().getRequestURI()); + } + throw new IllegalArgumentException("매핑되는 핸들러가 존재하지 않습니다."); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java new file mode 100644 index 0000000000..f59c0e4cc9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/handler/HandlerStatus.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.handler; + +import java.util.Objects; +import org.apache.coyote.http11.message.request.RequestLine; + +public class HandlerStatus { + private final String httpMethod; + private final String path; + + public HandlerStatus(final String httpMethod, final String path) { + this.httpMethod = httpMethod; + this.path = path; + } + + public static HandlerStatus from(final RequestLine requestLine) { + return new HandlerStatus(requestLine.getHttpMethod(), requestLine.getPath()); + } + + public String getHttpMethod() { + return httpMethod; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HandlerStatus)) { + return false; + } + final HandlerStatus that = (HandlerStatus) o; + return Objects.equals(httpMethod, that.httpMethod) && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(httpMethod, path); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java new file mode 100644 index 0000000000..f5ad90ef7c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/ContentType.java @@ -0,0 +1,46 @@ +package org.apache.coyote.http11.message; + +import java.util.Arrays; + +public enum ContentType { + HTML(".html", "text/html"), + CSS(".css", "text/css"), + JS(".js", "text/javascript"), + ICO(".ico", "image/x-icon"); + + private final String extension; + private final String headerValue; + + ContentType(final String extension, final String headerValue) { + this.extension = extension; + this.headerValue = headerValue; + } + + public static ContentType findByFileName(final String fileName) { + final String extension = getExtension(fileName); + return findByExtension(extension); + } + + public static String getExtension(final String fileName) { + final int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex > 0) { + return fileName.substring(lastDotIndex); + } + throw new IllegalArgumentException("파일 확장자가 존재하지 않습니다."); + } + + public static ContentType findByExtension(final String extension) { + return Arrays.stream(ContentType.values()) + .filter(contentType -> contentType.extension.equals(extension)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 파일형식입니다.")); + } + + public String getExtension() { + return extension; + } + + public String getHeaderValue() { + return headerValue; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java new file mode 100644 index 0000000000..8da09bf0de --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/HttpStatus.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.message; + +public enum HttpStatus { + OK("OK", 200), + FOUND("Found", 302), + UNAUTHORIZED("Unauthorized", 401); + + private final String message; + private final int code; + + HttpStatus(final String message, final int code) { + this.message = message; + this.code = code; + } + + public String getMessage() { + return message; + } + + public int getCode() { + return code; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java new file mode 100644 index 0000000000..d102690605 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Cookie.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.message.request; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +public class Cookie { + private final Map cookieData; + + private Cookie(final Map cookieData) { + this.cookieData = cookieData; + } + + public static Cookie from(final String value) { + if (value == null) { + return new Cookie(Collections.emptyMap()); + } + + final String[] cookies = value.split("; "); + final Map cookieData = Arrays.stream(cookies) + .map(cookie -> cookie.split("=")) + .collect(Collectors.toMap(cookiePair -> cookiePair[0], cookiePair -> cookiePair[1])); + + return new Cookie(cookieData); + } + + public String get(final String key) { + return cookieData.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java new file mode 100644 index 0000000000..c25d03e574 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/QueryParameter.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.message.request; + +import java.util.HashMap; +import java.util.Map; + +public class QueryParameter { + public static final QueryParameter EMPTY = new QueryParameter(new HashMap<>()); + private Map parameters = new HashMap<>(); + + public QueryParameter(final Map parameters) { + this.parameters = parameters; + } + + public QueryParameter(final String uri) { + final int index = uri.indexOf("?"); + final String queryString = uri.substring(index + 1); + final String[] splitedQueryStrings = queryString.split("&"); + for (final String str : splitedQueryStrings) { + final int strIndex = str.indexOf("="); + parameters.put(str.substring(0, strIndex), str.substring(strIndex + 1)); + } + } + + public Map getParameters() { + return parameters; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java new file mode 100644 index 0000000000..ab43ab12c8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/Request.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11.message.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Optional; +import org.apache.coyote.http11.Session; +import org.apache.coyote.http11.SessionManager; + +public class Request { + private final RequestLine requestLine; + private final RequestHeaders requestHeaders; + private final RequestForms requestForms; + + public Request(final RequestLine requestLine, final RequestHeaders requestHeaders, + final RequestForms requestForms) { + this.requestLine = requestLine; + this.requestHeaders = requestHeaders; + this.requestForms = requestForms; + } + + public static Request from(final BufferedReader br) throws IOException { + final RequestLine requestLine = RequestLine.from(br.readLine()); + final RequestHeaders requestHeaders = RequestHeaders.from(br); + final RequestForms requestForms = createRequestBody(br, requestHeaders); + return new Request(requestLine, requestHeaders, requestForms); + } + + private static RequestForms createRequestBody(final BufferedReader br, final RequestHeaders requestHeaders) + throws IOException { + if (!requestHeaders.hasContentType()) { + return new RequestForms(null); + } + final int contentLength = Integer.parseInt((String) requestHeaders.get("Content-Length")); + final char[] buffer = new char[contentLength]; + br.read(buffer, 0, contentLength); + final String requestBody = new String(buffer); + return RequestForms.from(requestBody); + } + + public boolean noSession() { + final String sessionId = requestHeaders.getCookieValue("JSESSIONID"); + return SessionManager.findSession(sessionId) == null; + } + + public Optional getSessionValue(final String key) { + final String sessionId = requestHeaders.getCookieValue("JSESSIONID"); + final Session session = SessionManager.findSession(sessionId); + if (session == null) { + return Optional.empty(); + } + return Optional.of(session.getAttribute(key)); + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public RequestHeaders getRequestHeaders() { + return requestHeaders; + } + + public RequestForms getRequestForms() { + return requestForms; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestForms.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestForms.java new file mode 100644 index 0000000000..02d40b0456 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestForms.java @@ -0,0 +1,27 @@ +package org.apache.coyote.http11.message.request; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class RequestForms { + private final Map formData; + + public RequestForms(final Map formData) { + this.formData = formData; + } + + public static RequestForms from(final String values) { + final Map requestForms = new LinkedHashMap<>(); + + final String[] splitedValues = values.split("&"); + for (final String value : splitedValues) { + final String[] formPair = value.split("="); + requestForms.put(formPair[0], formPair[1]); + } + return new RequestForms(requestForms); + } + + public Map getFormData() { + return formData; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestHeaders.java new file mode 100644 index 0000000000..f5b3513912 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestHeaders.java @@ -0,0 +1,37 @@ +package org.apache.coyote.http11.message.request; + +import java.io.BufferedReader; +import java.util.Map; +import java.util.stream.Collectors; + +public class RequestHeaders { + private final Map headers; + private final Cookie cookie; + + public RequestHeaders(final Map headers, final Cookie cookie) { + this.headers = headers; + this.cookie = cookie; + } + + public static RequestHeaders from(final BufferedReader br) { + final Map headers = br.lines() + .takeWhile(line -> !line.equals("")) + .map(line -> line.split(": ")) + .collect(Collectors.toMap(line -> line[0], line -> line[1])); + final String cookieValue = (String) headers.get("Cookie"); + headers.remove("Cookie"); + return new RequestHeaders(headers, Cookie.from(cookieValue)); + } + + boolean hasContentType() { + return headers.containsKey("Content-Type"); + } + + public Object get(final String headerKey) { + return headers.get(headerKey); + } + + public String getCookieValue(final String cookieKey) { + return cookie.get(cookieKey); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java new file mode 100644 index 0000000000..894580bf7d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestLine.java @@ -0,0 +1,43 @@ +package org.apache.coyote.http11.message.request; + +public class RequestLine { + private static final String REQUEST_HEADER_DELIMITER = " "; + private final String httpMethod; + private final RequestURI requestURI; + private final String httpVersion; + + public RequestLine(final String httpMethod, final RequestURI requestURI, final String httpVersion) { + this.httpMethod = httpMethod; + this.requestURI = requestURI; + this.httpVersion = httpVersion; + } + + public static RequestLine from(final String requestHeaderFirstLine) { + final String[] splitedLine = requestHeaderFirstLine.split(REQUEST_HEADER_DELIMITER); + return new RequestLine( + splitedLine[0], + new RequestURI(splitedLine[1]), + splitedLine[2] + ); + } + + public boolean isExistRequestFile() { + return this.requestURI.isExistFile(); + } + + public String getPath() { + return this.requestURI.getPath(); + } + + public RequestURI getRequestURI() { + return requestURI; + } + + public String getHttpMethod() { + return httpMethod; + } + + public String getHttpVersion() { + return httpVersion; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestURI.java b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestURI.java new file mode 100644 index 0000000000..e683f9c467 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/request/RequestURI.java @@ -0,0 +1,65 @@ +package org.apache.coyote.http11.message.request; + +import java.io.File; +import java.net.URL; +import java.util.Map; + +public class RequestURI { + private static final String ROOT_FOLDER = "static"; + private final String path; + private final QueryParameter queryParameter; + private final File file; + + public RequestURI(final String path, final QueryParameter queryParameter, final File file) { + this.path = path; + this.queryParameter = queryParameter; + this.file = file; + } + + public RequestURI(final String uri) { + this.path = parsePath(uri); + this.queryParameter = parseQueryString(uri); + + final URL resource = getClass().getClassLoader().getResource(ROOT_FOLDER + uri); + if (resource == null) { + this.file = null; + return; + } + final File findedFile = new File(resource.getPath()); + if (findedFile.isFile()) { + this.file = findedFile; + return; + } + this.file = null; + } + + private String parsePath(final String uri) { + final int index = uri.indexOf("?"); + if (index == -1) { + return uri; + } + + return uri.substring(0, index); + } + + private QueryParameter parseQueryString(final String uri) { + final int index = uri.indexOf("?"); + if (index == -1) { + return QueryParameter.EMPTY; + } + + return new QueryParameter(uri.substring(index + 1)); + } + + public boolean isExistFile() { + return this.file.isFile(); + } + + public String getPath() { + return this.path; + } + + public Map getQueryParameter() { + return queryParameter.getParameters(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/response/Response.java b/tomcat/src/main/java/org/apache/coyote/http11/message/response/Response.java new file mode 100644 index 0000000000..e2bae35ade --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/response/Response.java @@ -0,0 +1,88 @@ +package org.apache.coyote.http11.message.response; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.coyote.http11.message.ContentType; +import org.apache.coyote.http11.message.HttpStatus; +import org.apache.coyote.http11.message.request.RequestURI; + +public class Response { + private final String httpVersion; + private final HttpStatus httpStatus; + private final ResponseHeaders headers; + private final String responseBody; + + public Response(final String httpVersion, final HttpStatus httpStatus, final ResponseHeaders headers, + final String responseBody) { + this.httpVersion = httpVersion; + this.httpStatus = httpStatus; + this.headers = headers; + this.responseBody = responseBody; + } + + public static Response createByTemplate( + final HttpStatus httpStatus, + final String templateName, + final Map headers + ) { + final ResponseHeaders responseHeaders = new ResponseHeaders(); + final ContentType contentType = ContentType.findByFileName(templateName); + final String responseBody = getTemplateBody(templateName); + responseHeaders.add("Content-Type", contentType.getHeaderValue() + ";charset=utf-8"); + responseHeaders.add("Content-Length", String.valueOf(responseBody.getBytes().length)); + responseHeaders.addAll(headers); + return new Response("HTTP/1.1", httpStatus, responseHeaders, responseBody); + } + + private static String getTemplateBody(final String templatePath) { + final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + final URL templateUrl = classLoader.getResource("static/" + templatePath); + try { + return new String(Files.readAllBytes(new File(templateUrl.getFile()).toPath())); + } catch (final NullPointerException e) { + throw new IllegalArgumentException("파일이 존재하지 않습니다."); + } catch (final IOException e) { + throw new IllegalArgumentException("파일을 읽을 수 없습니다."); + } + } + + public static Response createByTemplate(final HttpStatus httpStatus, final String templateName) { + return createByTemplate(httpStatus, templateName, new HashMap<>()); + } + + public static Response createByTemplate(final RequestURI requestURI) { + final String templatePath = requestURI.getPath(); + final String responseBody = getTemplateBody(templatePath); + final ResponseHeaders responseHeaders = new ResponseHeaders(); + final ContentType contentType = ContentType.findByFileName(requestURI.getPath()); + responseHeaders.add("Content-Type", contentType.getHeaderValue() + ";charset=utf-8"); + responseHeaders.add("Content-Length", String.valueOf(responseBody.getBytes().length)); + return new Response("HTTP/1.1", HttpStatus.OK, responseHeaders, responseBody); + } + + public static Response createByResponseBody(final HttpStatus httpStatus, final String responseBody) { + final ResponseHeaders responseHeaders = new ResponseHeaders(); + responseHeaders.add("Content-Type", "text/html;charset=utf-8"); + responseHeaders.add("Content-Length", String.valueOf(responseBody.getBytes().length)); + return new Response("HTTP/1.1", httpStatus, responseHeaders, responseBody); + } + + public String getResponse() { + final List responseData = new ArrayList<>(); + final String responseLine = httpVersion + " " + httpStatus.getCode() + " " + httpStatus.getMessage() + " "; + responseData.add(responseLine); + + final List responseHeaderLines = headers.getHeaderLines(); + responseData.addAll(responseHeaderLines); + responseData.add(""); + + responseData.add(responseBody); + return String.join("\r\n", responseData); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/message/response/ResponseHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/message/response/ResponseHeaders.java new file mode 100644 index 0000000000..b89bdbdac4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/message/response/ResponseHeaders.java @@ -0,0 +1,31 @@ +package org.apache.coyote.http11.message.response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ResponseHeaders { + private final Map headers; + + public ResponseHeaders() { + this.headers = new HashMap<>(); + } + + public void add(final String key, final String value) { + if (headers.putIfAbsent(key, value) != null) { + final String originalValue = headers.get(key); + headers.replace(key, originalValue + "; " + value); + } + } + + public void addAll(final Map headers) { + headers.keySet().forEach(key -> add(key, headers.get(key))); + } + + public List getHeaderLines() { + return headers.keySet().stream() + .map(key -> key + ": " + headers.get(key) + " ") + .collect(Collectors.toList()); + } +} diff --git a/tomcat/src/main/resources/static/favicon.ico b/tomcat/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000..8f80fd22c8 Binary files /dev/null and b/tomcat/src/main/resources/static/favicon.ico differ 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..2efdaa905f 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,62 +1,65 @@ -package nextstep.org.apache.coyote.http11; - -import support.StubSocket; -import org.apache.coyote.http11.Http11Processor; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; - -import static org.assertj.core.api.Assertions.assertThat; - -class Http11ProcessorTest { - - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); - } - - @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 var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // 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())); - - assertThat(socket.output()).isEqualTo(expected); - } -} +package nextstep.org.apache.coyote.http11; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.List; +import org.apache.coyote.http11.Http11Processor; +import org.apache.coyote.http11.handler.HandlerMapper; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +class Http11ProcessorTest { + + @Test + void process() { + // given + final var socket = new StubSocket(); + final var handlerMapper = new HandlerMapper(); + final var processor = new Http11Processor(socket, handlerMapper); + + // when + processor.process(socket); + + // then + final List expectedLines = List.of( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: 12 \r\n", + "\r\nHello world!" + ); + + assertThat(socket.output()).contains(expectedLines); + } + + @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 var handlerMapper = new HandlerMapper(); + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket, handlerMapper); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + final List expectedLines = List.of( + "HTTP/1.1 200 OK \r\n", + "Content-Type: text/html;charset=utf-8 \r\n", + "Content-Length: 5564 \r\n", + new String(Files.readAllBytes(new File(resource.getFile()).toPath())) + ); + assertThat(socket.output()).contains(expectedLines); + } +}