diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..080c622473 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,54 +1,59 @@ 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.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +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 java.util.stream.Collectors; +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 URISyntaxException { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + File file = new File(getClass().getClassLoader().getResource("nextstep.txt").toURI()); + final String actual = file.getName(); 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; + String file = getClass().getClassLoader().getResource(fileName).getFile(); + Path path = Path.of(file); // todo - final List actual = Collections.emptyList(); + BufferedReader bufferedReader = Files.newBufferedReader(path); + final List actual = bufferedReader.lines().collect(Collectors.toList()); + // then assertThat(actual).containsOnly("nextstep"); } } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..3864070131 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,45 +1,56 @@ 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.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; 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 { + private static final String RESOURCE_PATH = "./src/test/resources/"; + /** * 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바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -48,11 +59,11 @@ class OutputStream_학습_테스트 { void OutputStream은_데이터를_바이트로_처리한다() throws IOException { final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); - /** * todo * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); @@ -61,13 +72,10 @@ class OutputStream_학습_테스트 { } /** - * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. - * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * - * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. - * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. - * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 - * 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. + * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. + *

+ * 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. Stream은 + * 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 데드락(deadlock) 상태가 되기 때문에 flush로 해제해야 한다. */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { @@ -79,13 +87,87 @@ class OutputStream_학습_테스트 { * ByteArrayOutputStream과 어떤 차이가 있을까? */ + outputStream.flush(); + verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } + @Test + void BufferdOutputStream은_특정_버퍼만큼_쌓이지_않았다면_반영하지_않는다() throws IOException { + // given + String testFilePath = makeTestFile(); + int inBufferDataSize = 4; + byte[] readData = readBytesFromNextStep(inBufferDataSize); + + int flushBufferSize = inBufferDataSize + 1; + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( + new FileOutputStream(testFilePath), flushBufferSize); + + // when + bufferedOutputStream.write(readData); + FileInputStream fileInputStream1 = new FileInputStream(testFilePath); + int charCount = 0; + while (fileInputStream1.read() != -1) { + charCount++; + } + + //then + assertThat(charCount).isZero(); + fileInputStream1.close(); + bufferedOutputStream.close(); + } + + private String makeTestFile() throws IOException { + String testFilePath = RESOURCE_PATH + "test.txt"; + + File testFile = new File(testFilePath); + if (testFile.exists()) { + testFile.delete(); + } + FileWriter fileWriter = new FileWriter(testFilePath); + fileWriter.write(""); + fileWriter.close(); + return testFilePath; + } + + private byte[] readBytesFromNextStep(int size) throws IOException { + File file = new File(RESOURCE_PATH + "nextstep.txt"); + FileInputStream fileInputStream = new FileInputStream(file); + BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); + byte[] readData = new byte[size]; + bufferedInputStream.read(readData); + fileInputStream.close(); + return readData; + } + + @Test + void BufferdOutputStream은_특정_버퍼만큼_쌓이면_반영한다() throws IOException { + // given + String testFilePath = makeTestFile(); + int inBufferDataSize = 4; + byte[] readData = readBytesFromNextStep(inBufferDataSize); + + int flushBufferSize = inBufferDataSize; + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(testFilePath), flushBufferSize); + + // when + bos.write(readData); + FileInputStream fis = new FileInputStream(testFilePath); + int charCount = 0; + while (fis.read() != -1) { + charCount++; + } + + //then + assertThat(charCount).isEqualTo(flushBufferSize); + fis.close(); + bos.close(); + } + /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -96,6 +178,8 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (outputStream) { + } verify(outputStream, atLeastOnce()).close(); } @@ -103,20 +187,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 +210,10 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + byte[] readByte = new byte[4]; + inputStream.read(readByte); + + String actual = new String(readByte); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -136,8 +221,7 @@ class InputStream_학습_테스트 { } /** - * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. - * 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. + * 스트림 사용이 끝나면 항상 close() 메서드를 호출하여 스트림을 닫는다. 장시간 스트림을 닫지 않으면 파일, 포트 등 다양한 리소스에서 누수(leak)가 발생한다. */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { @@ -148,6 +232,8 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try (inputStream) { + } verify(inputStream, atLeastOnce()).close(); } @@ -155,26 +241,24 @@ class InputStream_학습_테스트 { /** * FilterStream 학습하기 - * - * 필터는 필터 스트림, reader, writer로 나뉜다. - * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. - * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. + *

+ * 필터는 필터 스트림, reader, writer로 나뉜다. 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 + * 텍스트를 처리하는 데 사용된다. */ @Nested class FilterStream_학습_테스트 { /** - * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. - * InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. - * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? + * BufferedInputStream은 데이터 처리 속도를 높이기 위해 데이터를 버퍼에 저장한다. InputStream 객체를 생성하고 필터 생성자에 전달하면 필터에 연결된다. 버퍼 크기를 지정하지 + * 않으면 버퍼의 기본 사이즈는 얼마일까? */ @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,31 +266,31 @@ 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); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader); final StringBuilder actual = new StringBuilder(); + bufferedReader.lines().forEach(s -> actual.append(s + "\r\n")); + assertThat(actual).hasToString(emoji); } } diff --git a/tomcat/README.md b/tomcat/README.md new file mode 100644 index 0000000000..52bb930f2a --- /dev/null +++ b/tomcat/README.md @@ -0,0 +1,20 @@ +## Mission 1 : HTTP 서버 구현하기 + +1. [x] Get /index.html 응답하기 +2. [x] CSS 지원하기 + - [x] 정적 파일을 지원하는 ResourceProvider 를 생성한다. + - [x] 해당 정적 파일을 읽어온다. + - [x] 해당 정적 파일의 타입을 반환한다. +3. [x] Query String 파싱 + - [x] 먼저 정적 파일을 찾는다. + - [x] 요청을 handle 할 수 있는 핸들러를 찾는다. + - [x] 아무것도 못 찾았다면 not Found 페이지 반환. + +## Mission 2 : 로그인 구현하기 + +1. [x] HTTP Status Code 302 +2. [x] POST 방식으로 회원가입 및 로그인 + - [x] Request 를 생성한다 + - [x] 요청을 해결할 수 있는 Contoller 를 Mapper 로 엮어둔다. +3. [x] Cookie에 JSESSIONID 값 저장하기 +4. [x] Session 구현하기 diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..d35eb9dbf6 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -1,27 +1,29 @@ package nextstep.jwp.db; -import nextstep.jwp.model.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import nextstep.jwp.model.User; public class InMemoryUserRepository { private static final Map database = new ConcurrentHashMap<>(); + private static Long publishedId = 1L; static { final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); database.put(user.getAccount(), user); } - public static void save(User user) { - database.put(user.getAccount(), user); + private InMemoryUserRepository() { } public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - private InMemoryUserRepository() {} + public static void save(User user) { + User saveUser = new User(++publishedId, user.getAccount(), user.getPassword(), user.getEmail()); + database.put(saveUser.getAccount(), saveUser); + } } diff --git a/tomcat/src/main/java/nextstep/jwp/model/User.java b/tomcat/src/main/java/nextstep/jwp/model/User.java index 4c2a2cd184..06dc83367a 100644 --- a/tomcat/src/main/java/nextstep/jwp/model/User.java +++ b/tomcat/src/main/java/nextstep/jwp/model/User.java @@ -22,17 +22,29 @@ public boolean checkPassword(String password) { return this.password.equals(password); } + public Long getId() { + return id; + } + public String getAccount() { return account; } + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + @Override public String toString() { return "User{" + - "id=" + id + - ", account='" + account + '\'' + - ", email='" + email + '\'' + - ", password='" + password + '\'' + - '}'; + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 7f1b2c7e96..1dba43ac70 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,21 +1,28 @@ 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.controller.HandlerMapper; +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 ResourceProvider resourceProvider; + private final HandlerMapper handlerMapper; public Http11Processor(final Socket connection) { this.connection = connection; + this.resourceProvider = new ResourceProvider(); + this.handlerMapper = new HandlerMapper(this.resourceProvider); } @Override @@ -26,22 +33,29 @@ 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); - + try (final var inputReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + final var outputStream = connection.getOutputStream()) { + HttpRequest httpRequest = HttpRequest.makeRequest(inputReader); + String response = getResponse(httpRequest); outputStream.write(response.getBytes()); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private String getResponse(HttpRequest httpRequest) { + if (resourceProvider.haveResource(httpRequest.getRequestLine().getPath())) { + return resourceProvider.staticResourceResponse(httpRequest.getRequestLine().getPath()); + } + if (handlerMapper.haveAvailableHandler(httpRequest)) { + return handlerMapper.controllerResponse(httpRequest); + } + return String.join("\r\n", + "HTTP/1.1 200 OK ", + "Content-Type: text/html;charset=utf-8 ", + "Content-Length: 12 ", + "", + "Hello world!"); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ResourceProvider.java b/tomcat/src/main/java/org/apache/coyote/http11/ResourceProvider.java new file mode 100644 index 0000000000..cd9c80797e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ResourceProvider.java @@ -0,0 +1,69 @@ +package org.apache.coyote.http11; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.Optional; + +public class ResourceProvider { + + public boolean haveResource(String resourcePath) { + return findFileURL(resourcePath).isPresent(); + } + + public String resourceBodyOf(String resourcePath) { + try { + Optional fileURL = findFileURL(resourcePath); + if (fileURL.isPresent()) { + URL url = fileURL.get(); + return new String(Files.readAllBytes(new File(url.getFile()).toPath())); + } + throw new IllegalArgumentException("파일이 존재하지 않습니다."); + } catch (IOException e) { + throw new IllegalArgumentException("존재하지 않는 파일 입니다."); + } + } + + private Optional findFileURL(String resourcePath) { + URL resourceURL = getClass().getClassLoader().getResource("static" + resourcePath); + if (resourceURL == null) { + return Optional.empty(); + } + if (new File(resourceURL.getFile()).isDirectory()) { + return Optional.empty(); + } + return Optional.of(resourceURL); + } + + public String contentTypeOf(String resourcePath) { + File file = getFile(resourcePath); + String fileName = file.getName(); + if (fileName.endsWith(".js")) { + return "Content-Type: text/javascript "; + } + if (fileName.endsWith(".css")) { + return "Content-Type: text/css;charset=utf-8"; + } + if (fileName.endsWith(".html")) { + return "Content-Type: text/html;charset=utf-8 "; + } + return "Content-Type: text/plain"; + } + + private File getFile(String resourcePath) { + return new File( + findFileURL(resourcePath).orElseThrow((() -> new IllegalArgumentException("파일이 존재하지 않습니다."))) + .getFile()); + } + + public String staticResourceResponse(String resourcePath) { + String responseBody = resourceBodyOf(resourcePath); + return String.join("\r\n", + "HTTP/1.1 200 OK ", + contentTypeOf(resourcePath), + "Content-Length: " + responseBody.getBytes().length + " ", + "", + responseBody); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java new file mode 100644 index 0000000000..d80994d5ad --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/Controller.java @@ -0,0 +1,9 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public interface Controller { + + HttpResponse handle(HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/HandlerMapper.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/HandlerMapper.java new file mode 100644 index 0000000000..c75bbbe67a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/HandlerMapper.java @@ -0,0 +1,126 @@ +package org.apache.coyote.http11.controller; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Collectors; +import org.apache.coyote.http11.ResourceProvider; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.response.HttpStatusCode; +import org.apache.coyote.http11.service.LoginService; + +public class HandlerMapper { + + private final Map controllerByMapper = new HashMap<>(); + private final ResourceProvider resourceProvider; + + public HandlerMapper(ResourceProvider resourceProvider) { + enrollHandler(); + this.resourceProvider = resourceProvider; + } + + private void enrollHandler() { + controllerByMapper.put( + request -> "/login".equals(request.getRequestLine().getPath()) && + HttpMethod.POST.equals(request.getRequestLine().getMethod()), + new LoginController(new LoginService())); + + controllerByMapper.put( + request -> "/register".equals(request.getRequestLine().getPath()) && + HttpMethod.POST.equals(request.getRequestLine().getMethod()), + new SignUpController(new LoginService())); + + controllerByMapper.put( + request -> "/login".equals(request.getRequestLine().getPath()) && + HttpMethod.GET.equals(request.getRequestLine().getMethod()), + new LoginViewController()); + + controllerByMapper.put( + request -> "/register".equals(request.getRequestLine().getPath()) && + HttpMethod.GET.equals(request.getRequestLine().getMethod()), + new SignUpViewController()); + } + + public boolean haveAvailableHandler(HttpRequest httpRequest) { + return controllerByMapper.keySet().stream() + .anyMatch(mapper -> mapper.canHandle(httpRequest)); + } + + private Controller getHandler(HttpRequest httpRequest) { + Mapper mapper = controllerByMapper.keySet().stream() + .filter(mp -> mp.canHandle(httpRequest)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 요청을 해결할 수 있는 핸들러가 없습니다.")); + return controllerByMapper.get(mapper); + } + + public String controllerResponse(HttpRequest httpRequest) { + Controller handler = getHandler(httpRequest); + HttpResponse httpResponse = (HttpResponse) handler.handle(httpRequest); + return makeResponse(httpResponse); + } + + private String makeResponse(HttpResponse httpResponse) { + StringBuilder response = new StringBuilder(); + response.append(requestLine(httpResponse)); + Optional body = bodyOf(httpResponse); + if (body.isPresent()) { + return response.append(responseWithBody(httpResponse, body.get())).toString(); + } + String str = responseWithoutBody(httpResponse); + response.append("\r\n"); + return response.append(str).toString(); + } + + + private String requestLine(HttpResponse httpResponse) { + HttpStatusCode httpStatusCode = HttpStatusCode.of(httpResponse.getStatusCode()); + return "HTTP/1.1 " + httpStatusCode.getStatusCode() + " " + httpStatusCode.name() + " "; + } + + private Optional bodyOf(HttpResponse httpResponse) { + if (httpResponse.isViewResponse()) { + return Optional.of(resourceProvider.resourceBodyOf(httpResponse.getViewPath())); + } + return Optional.empty(); + } + + private String responseWithBody(HttpResponse httpResponse, String body) { + Map headers = httpResponse.getHeaders(); + StringJoiner stringJoiner = new StringJoiner("\r\n"); + stringJoiner.add(headerResponse(headers)); + stringJoiner.add(resourceProvider.contentTypeOf(httpResponse.getViewPath())); + stringJoiner.add("Content-Length: " + body.getBytes().length + " "); + stringJoiner.add(""); + stringJoiner.add(body); + return stringJoiner.toString(); + } + + private String responseWithoutBody(HttpResponse httpResponse) { + Map headers = httpResponse.getHeaders(); + StringJoiner stringJoiner = new StringJoiner("\r\n"); + stringJoiner.add(headerResponse(headers)); + stringJoiner.add(""); + return stringJoiner.toString(); + } + + private String headerResponse(Map headers) { + return headers.keySet() + .stream() + .map(headerName -> makeHeader(headerName, headers.get(headerName))) + .collect(Collectors.joining("\r\n")); + } + + private String makeHeader(String headerName, String value) { + return headerName + ": " + value; + } + + @FunctionalInterface + private interface Mapper { + + Boolean canHandle(HttpRequest httpRequest); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java new file mode 100644 index 0000000000..be52b2ad57 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginController.java @@ -0,0 +1,53 @@ +package org.apache.coyote.http11.controller; + +import java.util.Map; +import java.util.Optional; +import org.apache.coyote.http11.controller.util.BodyExtractor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.service.LoginService; +import org.apache.coyote.http11.session.SessionManager; + +public class LoginController implements Controller { + + private static final String ACCOUNT = "account"; + private static final String PASSWORD = "password"; + private static final String LOCATION_HEADER = "Location"; + + private final LoginService loginService; + + public LoginController(LoginService loginService) { + this.loginService = loginService; + } + + @Override + public HttpResponse handle(HttpRequest httpRequest) { + if (SessionManager.loggedIn(httpRequest)) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .build(); + } + + Optional loginSession = login(httpRequest); + if (loginSession.isPresent()) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .addHeader("Set-Cookie", "JSESSIONID" + "=" + loginSession.get()) + .build(); + } + + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/401.html") + .build(); + } + + private Optional login(HttpRequest httpRequest) { + Map bodyData = BodyExtractor.convertBody(httpRequest.getResponseBody()); + String account = bodyData.get(ACCOUNT); + String password = bodyData.get(PASSWORD); + if (account != null && password != null) { + return loginService.login(account, password); + } + return Optional.empty(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginViewController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginViewController.java new file mode 100644 index 0000000000..4a7874a658 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/LoginViewController.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.session.SessionManager; + +public class LoginViewController implements Controller { + + private static final String LOCATION_HEADER = "Location"; + + @Override + public HttpResponse handle(HttpRequest httpRequest) { + if (SessionManager.loggedIn(httpRequest)) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .build(); + } + HttpResponse httpResponse = HttpResponse.status(200).build(); + httpResponse.responseView("/login.html"); + return httpResponse; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpController.java new file mode 100644 index 0000000000..d1aaa71b67 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpController.java @@ -0,0 +1,51 @@ +package org.apache.coyote.http11.controller; + +import java.util.Map; +import org.apache.coyote.http11.controller.util.BodyExtractor; +import org.apache.coyote.http11.exception.MemberAlreadyExistsException; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.service.LoginService; +import org.apache.coyote.http11.session.SessionManager; + +public class SignUpController implements Controller { + + private static final String ACCOUNT = "account"; + private static final String PASSWORD = "password"; + private static final String EMAIL = "email"; + private static final String LOCATION_HEADER = "Location"; + + private final LoginService loginService; + + public SignUpController(LoginService loginService) { + this.loginService = loginService; + } + + @Override + public HttpResponse handle(HttpRequest httpRequest) { + try { + if (SessionManager.loggedIn(httpRequest)) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .build(); + } + String loginSession = signUp(httpRequest); + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .addHeader("Set-Cookie", "JSESSIONID" + "=" + loginSession) + .build(); + } catch (MemberAlreadyExistsException e) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/register.html") + .build(); + } + } + + private String signUp(HttpRequest httpRequest) { + Map bodyData = BodyExtractor.convertBody(httpRequest.getResponseBody()); + String account = bodyData.get(ACCOUNT); + String password = bodyData.get(PASSWORD); + String email = bodyData.get(EMAIL); + return loginService.signUp(account, password, email); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpViewController.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpViewController.java new file mode 100644 index 0000000000..df4302ce70 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/SignUpViewController.java @@ -0,0 +1,22 @@ +package org.apache.coyote.http11.controller; + +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.apache.coyote.http11.session.SessionManager; + +public class SignUpViewController implements Controller { + + private static final String LOCATION_HEADER = "Location"; + + @Override + public HttpResponse handle(HttpRequest httpRequest) { + if (SessionManager.loggedIn(httpRequest)) { + return HttpResponse.status(302) + .addHeader(LOCATION_HEADER, "/index.html") + .build(); + } + HttpResponse httpResponse = HttpResponse.status(200).build(); + httpResponse.responseView("/register.html"); + return httpResponse; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/controller/util/BodyExtractor.java b/tomcat/src/main/java/org/apache/coyote/http11/controller/util/BodyExtractor.java new file mode 100644 index 0000000000..2293a5fed9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/controller/util/BodyExtractor.java @@ -0,0 +1,42 @@ +package org.apache.coyote.http11.controller.util; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.coyote.http11.request.RequestBody; + +public class BodyExtractor { + + private static final String INVIDUAL_QUERY_PARAM_DIVIDER = "&"; + private static final String QUERY_PARAM_KEY_VALUE_SPLIT = "="; + private static final int DONT_HAVE_VALUE = 1; + + private BodyExtractor() { + } + + public static Map convertBody(RequestBody requestBody) { + Optional body = requestBody.getBody(); + if (body.isEmpty()) { + throw new IllegalArgumentException("Body 에 데이터가 없습니다."); + } + return Stream.of(body.get().split(INVIDUAL_QUERY_PARAM_DIVIDER)) + .collect(Collectors.toMap(BodyExtractor::keyOf, BodyExtractor::valueOf)); + } + + private static String keyOf(String qp) { + if (!qp.contains(QUERY_PARAM_KEY_VALUE_SPLIT)) { + throw new IllegalArgumentException("유효하지 않은 key value 형식 입니다."); + } + String[] split = qp.split(QUERY_PARAM_KEY_VALUE_SPLIT); + return split[0]; + } + + private static String valueOf(String qp) { + String[] split = qp.split(QUERY_PARAM_KEY_VALUE_SPLIT); + if (split.length == DONT_HAVE_VALUE) { + return ""; + } + return split[1]; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/exception/MemberAlreadyExistsException.java b/tomcat/src/main/java/org/apache/coyote/http11/exception/MemberAlreadyExistsException.java new file mode 100644 index 0000000000..0c9013204b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/exception/MemberAlreadyExistsException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.http11.exception; + +public class MemberAlreadyExistsException extends RuntimeException { + + public MemberAlreadyExistsException(String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Cookies.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Cookies.java new file mode 100644 index 0000000000..7021d493cc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Cookies.java @@ -0,0 +1,47 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Cookies { + + private static final String SESSION_COOKIE = "JSESSIONID"; + private static final String COOKIE_SPLITER = ";"; + private static final String COOKIE_VALUE_SPLITER = "="; + + private final Map cookieValues; + + public Cookies(String cookies) { + this.cookieValues = Stream.of(cookies.split(COOKIE_SPLITER)) + .collect(Collectors.toMap( + this::extractKey, + this::extractValue + )); + } + + private String extractKey(String value) { + String[] split = value.split(COOKIE_VALUE_SPLITER); + return split[0].trim(); + } + + private String extractValue(String value) { + String[] split = value.split(COOKIE_VALUE_SPLITER); + return split[1].trim(); + } + + public Optional getCookieOf(String cookieName) { + if (!cookieValues.containsKey(cookieName)) { + return Optional.empty(); + } + return Optional.of(cookieValues.get(cookieName)); + } + + public Optional getSessionCookie() { + if (!cookieValues.containsKey(SESSION_COOKIE)) { + return Optional.empty(); + } + return Optional.of(cookieValues.get(SESSION_COOKIE)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..0ef37c0c97 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,20 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; + +public enum HttpMethod { + GET, + POST, + PUT, + DELETE; + + public static HttpMethod of(String targetMethod) { + if (targetMethod == null) { + throw new IllegalArgumentException("null 에 대한 메서드는 존재하지 않습니다."); + } + return Arrays.stream(HttpMethod.values()) + .filter(httpMethod -> targetMethod.toUpperCase().equals(httpMethod.name())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 메서드 입니다.")); + } +} 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..a064f69ad3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,74 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; + +public class HttpRequest { + + private final RequestLine requestLine; + private final RequestHeaders requestHeaders; + private final RequestBody requestBody; + + public HttpRequest(RequestLine requestLine, RequestHeaders requestHeaders, RequestBody requestBody) { + this.requestLine = requestLine; + this.requestHeaders = requestHeaders; + this.requestBody = requestBody; + } + + public static HttpRequest makeRequest(BufferedReader inputReader) { + RequestLine requestLine = new RequestLine(readRequestLine(inputReader)); + RequestHeaders requestHeaders = new RequestHeaders(readHeaders(inputReader)); + RequestBody requestBody = new RequestBody(readBody(inputReader, requestHeaders)); + return new HttpRequest(requestLine, requestHeaders, requestBody); + } + + private static String readRequestLine(BufferedReader inputReader) { + try { + return inputReader.readLine(); + } catch (IOException e) { + throw new IllegalArgumentException("RequestLine 을 읽을 수 없습니다."); + } + } + + private static String readHeaders(BufferedReader inputReader) { + try { + StringBuilder stringBuilder = new StringBuilder(); + String line = inputReader.readLine(); + while (!"".equals(line)) { + stringBuilder.append(line); + stringBuilder.append(System.lineSeparator()); + line = inputReader.readLine(); + } + return stringBuilder.toString(); + } catch (IOException e) { + throw new IllegalArgumentException("RequestHeader 를 읽을 수 없습니다."); + } + } + + private static String readBody(BufferedReader inputReader, RequestHeaders requestHeaders) { + try { + String s = requestHeaders.getHeaders().get("Content-Length"); + if (s == null) { + return null; + } + int contentLength = Integer.parseInt(s); + char[] buffer = new char[contentLength]; + inputReader.read(buffer, 0, contentLength); + return new String(buffer); + } catch (IOException e) { + throw new IllegalArgumentException("RequestBody 를 읽을 수 없습니다."); + } + } + + public RequestLine getRequestLine() { + return requestLine; + } + + public RequestHeaders getRequestHeaders() { + return requestHeaders; + } + + public RequestBody getResponseBody() { + return requestBody; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java new file mode 100644 index 0000000000..0761570f63 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java @@ -0,0 +1,16 @@ +package org.apache.coyote.http11.request; + +import java.util.Optional; + +public class RequestBody { + + String body; + + public RequestBody(String body) { + this.body = body; + } + + public Optional getBody() { + return Optional.ofNullable(body); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeaders.java new file mode 100644 index 0000000000..effda37690 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeaders.java @@ -0,0 +1,60 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RequestHeaders { + + private static final String HEADER_SEPARATOR = System.lineSeparator(); + private static final String HEADER_KEY_VALUE_SPLIT = ":"; + private static final String COOKIE = "Cookie"; + + private final Map headers; + private final Optional cookies; + + public RequestHeaders(String headers) { + Map headerValues = extractHeader(headers); + this.headers = headerValues; + this.cookies = extractCookie(headerValues); + } + + private Optional extractCookie(Map headerValues) { + String cookieHeader = headerValues.get(COOKIE); + if (cookieHeader == null) { + return Optional.empty(); + } + return Optional.of(new Cookies(cookieHeader)); + } + + private Map extractHeader(String headers) { + return Stream.of(headers.split(HEADER_SEPARATOR)) + .collect(Collectors.toMap( + this::keyOf, + this::valueOf + )); + } + + private String keyOf(String header) { + String[] split = header.split(HEADER_KEY_VALUE_SPLIT); + return split[0].trim(); + } + + private String valueOf(String header) { + String[] split = header.split(HEADER_KEY_VALUE_SPLIT); + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 1; i < split.length; i++) { + stringBuilder.append(split[i]); + } + return stringBuilder.toString().trim(); + } + + public Optional getCookie() { + return cookies; + } + + public Map getHeaders() { + return headers; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java new file mode 100644 index 0000000000..8d37131c35 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,86 @@ +package org.apache.coyote.http11.request; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RequestLine { + + private static final int REQUEST_LINE_COMPONENT = 3; + private static final String INVIDUAL_QUERY_PARAM_DIVIDER = "&"; + private static final String QUERY_PARAM_KEY_VALUE_SPLIT = "="; + private static final int DONT_HAVE_VALUE = 1; + private static final String QUERY_PARAM_SPLITER = "\\?"; + private static final String QUERY_PARAM = "?"; + + private final HttpMethod httpMethod; + private final String path; + private final Map queryParam; + private final String httpVersion; + + public RequestLine(String requestLine) { + String[] split = requestLine.split(" "); + validate(split); + this.httpMethod = methodOf(split[0]); + this.path = pathOf(split[1]); + this.queryParam = queryParamsOf(split[1]); + this.httpVersion = split[2]; + } + + private void validate(String[] split) { + if (split.length != REQUEST_LINE_COMPONENT) { + throw new IllegalArgumentException("http 요청 라인은 3가지 요소로 구성되어야 합니다."); + } + } + + private HttpMethod methodOf(String methodName) { + return HttpMethod.of(methodName); + } + + private String pathOf(String requestTarget) { + String[] split = requestTarget.split(QUERY_PARAM_SPLITER); + return split[0]; + } + + private Map queryParamsOf(String requestTarget) { + if (!requestTarget.contains(QUERY_PARAM)) { + return Collections.emptyMap(); + } + String[] split = requestTarget.split(QUERY_PARAM_SPLITER); + return Stream.of(split[1].split(INVIDUAL_QUERY_PARAM_DIVIDER)) + .collect(Collectors.toMap(this::keyOf, this::valueOf)); + } + + private String keyOf(String qp) { + if (!qp.contains(QUERY_PARAM_KEY_VALUE_SPLIT)) { + throw new IllegalArgumentException("유효하지 않은 key value 형식 입니다."); + } + String[] split = qp.split(QUERY_PARAM_KEY_VALUE_SPLIT); + return split[0]; + } + + private String valueOf(String qp) { + String[] split = qp.split(QUERY_PARAM_KEY_VALUE_SPLIT); + if (split.length == DONT_HAVE_VALUE) { + return ""; + } + return split[1]; + } + + public HttpMethod getMethod() { + return httpMethod; + } + + public String getPath() { + return path; + } + + public Map getQueryParam() { + return queryParam; + } + + public String getHttpVersion() { + return httpVersion; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..1de50e5ae8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,84 @@ +package org.apache.coyote.http11.response; + +import java.util.HashMap; +import java.util.Map; + +public class HttpResponse { + + private final int statusCode; + private final Map headers; + private final T body; + private boolean viewResponse = false; + private String viewPath = null; + + private HttpResponse(int statusCode, Map headers, T body) { + this.statusCode = statusCode; + this.headers = headers; + this.body = body; + } + + public static ResponseBuilder status(int statusCode) { + return new ResponseBuilderImpl(statusCode); + } + + public void responseView(String viewPath) { + this.viewResponse = true; + this.viewPath = viewPath; + } + + public int getStatusCode() { + return statusCode; + } + + public Map getHeaders() { + return headers; + } + + public T getBody() { + return body; + } + + public boolean isViewResponse() { + return viewResponse; + } + + public String getViewPath() { + return viewPath; + } + + public interface ResponseBuilder { + + ResponseBuilder addHeader(String key, String value); + + HttpResponse build(); + + HttpResponse body(T body); + } + + private static final class ResponseBuilderImpl implements ResponseBuilder { + + private int statusCode; + private Map headers; + + public ResponseBuilderImpl(int statusCode) { + this.statusCode = statusCode; + this.headers = new HashMap<>(); + } + + @Override + public ResponseBuilder addHeader(String key, String value) { + headers.put(key, value); + return this; + } + + @Override + public HttpResponse build() { + return body(null); + } + + @Override + public HttpResponse body(T body) { + return new HttpResponse<>(this.statusCode, headers, body); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java new file mode 100644 index 0000000000..89a49cb22c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.response; + +import java.util.stream.Stream; + +public enum HttpStatusCode { + OK(200), + FOUND(302); + + private final int statusCode; + + HttpStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public static HttpStatusCode of(int statusCode) { + return Stream.of(HttpStatusCode.values()) + .filter(httpStatusCode -> httpStatusCode.statusCode == statusCode) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재 하지 않는 상태 코드 입니다.")); + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/service/LoginService.java b/tomcat/src/main/java/org/apache/coyote/http11/service/LoginService.java new file mode 100644 index 0000000000..0bb60cb0e1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/service/LoginService.java @@ -0,0 +1,49 @@ +package org.apache.coyote.http11.service; + +import java.util.Optional; +import java.util.UUID; +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.exception.MemberAlreadyExistsException; +import org.apache.coyote.http11.session.SessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginService { + + private static final Logger log = LoggerFactory.getLogger(LoginService.class); + + public Optional login(String account, String password) { + Optional user = InMemoryUserRepository.findByAccount(account); + if (user.isPresent() && checkPassword(user.get(), password)) { + String sessionId = makeRandomUUID(); + SessionManager.enrollSession(user.get(), sessionId); + return Optional.of(sessionId); + } + return Optional.empty(); + } + + private String makeRandomUUID() { + return UUID.randomUUID().toString(); + } + + private boolean checkPassword(User user, String password) { + boolean validLogin = user.checkPassword(password); + if (validLogin) { + log.info("{}", user); + } + return validLogin; + } + + public String signUp(String account, String password, String email) { + Optional user = InMemoryUserRepository.findByAccount(account); + if (user.isPresent()) { + throw new MemberAlreadyExistsException("이미 존재하는 유저입니다."); + } + User newUser = new User(account, password, email); + InMemoryUserRepository.save(newUser); + String sessionId = makeRandomUUID(); + SessionManager.enrollSession(newUser, sessionId); + return sessionId; + } +} 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..45c421f410 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/Session.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public Object getAttribute(String name) { + return values.get(name); + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } + + public boolean isNew() { + throw new UnsupportedOperationException(); + } +} 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..37d687e200 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import nextstep.jwp.model.User; +import org.apache.coyote.http11.request.Cookies; +import org.apache.coyote.http11.request.HttpRequest; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + public static boolean loggedIn(HttpRequest httpRequest) { + Optional cookie = httpRequest.getRequestHeaders().getCookie(); + if (cookie.isPresent()) { + return checkSession(cookie.get()); + } + return false; + } + + private static boolean checkSession(Cookies cookies) { + Optional sessionCookie = cookies.getSessionCookie(); + if (sessionCookie.isPresent()) { + String sessionId = sessionCookie.get(); + Session session = SESSIONS.get(sessionId); + return session != null; + } + return false; + } + + public static void enroll(Session session) { + SESSIONS.put(session.getId(), session); + } + + public static void enrollSession(User user, String sessionId) { + Session session = new Session(sessionId); + session.setAttribute("user", user); + SessionManager.enroll(session); + } + + public void add(Session session) { + SESSIONS.put(session.getId(), session); + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..2563f783e8 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,19 +20,25 @@

로그인

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