diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..4890d6fc72 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -2,9 +2,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -16,39 +23,59 @@ @DisplayName("File 클래스 학습 테스트") class FileTest { + private static Logger log = LoggerFactory.getLogger(FileTest.class); + /** * resource 디렉터리 경로 찾기 - * + *

* File 객체를 생성하려면 파일의 경로를 알아야 한다. * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { + // given final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + // when + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + final URL url = classLoader.getResource(fileName); + final String actual = url.getPath(); + + log.info("=================> classLoader = {}", classLoader); + log.info("=================> url = {}", url); + log.info("=================> path = {}", actual); + // then assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * + *

* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws URISyntaxException, IOException { + // given final String fileName = "nextstep.txt"; - // todo - final Path path = null; + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + final URI uri = classLoader.getResource(fileName).toURI(); + final File file = new File(uri); + final Path path = file.getAbsoluteFile().toPath(); + + // when + final List actual = Files.readAllLines(path); - // todo - final List actual = Collections.emptyList(); + log.info("=================> classLoader = {}", classLoader); + log.info("=================> uri = {}", uri); + log.info("=================> file = {}", file); + log.info("=================> path = {}", path); + log.info("=================> actual = {}", actual); + // 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..232a62d16f 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -3,20 +3,38 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.*; +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.util.List; +import java.util.stream.Collectors; +import static java.nio.charset.StandardCharsets.UTF_16; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * 자바는 스트림(Stream)으로부터 I/O를 사용한다. * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * + *

* InputStream은 데이터를 읽고, OutputStream은 데이터를 쓴다. * FilterStream은 InputStream이나 OutputStream에 연결될 수 있다. * FilterStream은 읽거나 쓰는 데이터를 수정할 때 사용한다. (e.g. 암호화, 압축, 포맷 변환) - * + *

* Stream은 데이터를 바이트로 읽고 쓴다. * 바이트가 아닌 텍스트(문자)를 읽고 쓰려면 Reader와 Writer 클래스를 연결한다. * Reader, Writer는 다양한 문자 인코딩(e.g. UTF-8)을 처리할 수 있다. @@ -24,9 +42,11 @@ @DisplayName("Java I/O Stream 클래스 학습 테스트") class IOStreamTest { + private static Logger log = LoggerFactory.getLogger(IOStreamTest.class); + /** * OutputStream 학습하기 - * + *

* 자바의 기본 출력 클래스는 java.io.OutputStream이다. * OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; @@ -39,23 +59,27 @@ class OutputStream_학습_테스트 { * OutputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 쓰기 위해 write(int b) 메서드를 사용한다. * 예를 들어, FilterOutputStream은 파일로 데이터를 쓸 때, * 또는 DataOutputStream은 자바의 primitive type data를 다른 매체로 데이터를 쓸 때 사용한다. - * + *

* write 메서드는 데이터를 바이트로 출력하기 때문에 비효율적이다. * write(byte[] data)write(byte b[], int off, int len) 메서드는 * 1바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. */ @Test 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 메서드를 사용해서 테스트를 통과시킨다 */ + // given + final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; + final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); + + // when + outputStream.write(bytes); final String actual = outputStream.toString(); + // then assertThat(actual).isEqualTo("nextstep"); outputStream.close(); } @@ -63,7 +87,7 @@ class OutputStream_학습_테스트 { /** * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * + *

* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 @@ -71,14 +95,19 @@ class OutputStream_학습_테스트 { */ @Test void BufferedOutputStream을_사용하면_버퍼링이_가능하다() throws IOException { - final OutputStream outputStream = mock(BufferedOutputStream.class); - /** * todo * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ + // given + final OutputStream outputStream = mock(BufferedOutputStream.class); + + // when + outputStream.flush(); + + // then verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } @@ -89,26 +118,35 @@ class OutputStream_학습_테스트 { */ @Test void OutputStream은_사용하고_나서_close_처리를_해준다() throws IOException { - final OutputStream outputStream = mock(OutputStream.class); - /** * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + // given + OutputStream outputStream = mock(OutputStream.class); + + // when + try (outputStream) { + outputStream.flush(); + } catch (IOException e) { + throw new AssertionError(); + } + + // then verify(outputStream, atLeastOnce()).close(); } } /** * InputStream 학습하기 - * + *

* 자바의 기본 입력 클래스는 java.io.InputStream이다. * InputStream은 다른 매체로부터 바이트로 데이터를 읽을 때 사용한다. * InputStream의 read() 메서드는 기반 메서드이다. * public abstract int read() throws IOException; - * + *

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested @@ -121,17 +159,24 @@ class InputStream_학습_테스트 { */ @Test void InputStream은_데이터를_바이트로_읽는다() throws IOException { - byte[] bytes = {-16, -97, -92, -87}; - final InputStream inputStream = new ByteArrayInputStream(bytes); - /** * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; - assertThat(actual).isEqualTo("🤩"); - assertThat(inputStream.read()).isEqualTo(-1); + // given + byte[] bytes = {-16, -97, -92, -87}; + final InputStream inputStream = new ByteArrayInputStream(bytes); + + // when + final String actual = new String(inputStream.readAllBytes()); + + // then + assertAll( + () -> assertThat(actual).isEqualTo("🤩"), + () -> assertThat(inputStream.read()).isEqualTo(-1) + ); + inputStream.close(); } @@ -141,21 +186,30 @@ class InputStream_학습_테스트 { */ @Test void InputStream은_사용하고_나서_close_처리를_해준다() throws IOException { - final InputStream inputStream = mock(InputStream.class); - /** * todo * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + // given + final InputStream inputStream = mock(InputStream.class); + + // when + try (inputStream) { + inputStream.readAllBytes(); + } catch (IOException e) { + throw new AssertionError(); + } + + // then verify(inputStream, atLeastOnce()).close(); } } /** * FilterStream 학습하기 - * + *

* 필터는 필터 스트림, reader, writer로 나뉜다. * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. @@ -169,13 +223,21 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { + // given final String text = "필터에 연결해보자."; - final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream inputStream = new ByteArrayInputStream(text.getBytes(UTF_8)); + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + log.info("===========> inputStream = {}", new ByteArrayInputStream(text.getBytes(UTF_8)).readAllBytes()); + log.info("===========> bufferedInputStream = {}", new BufferedInputStream(new ByteArrayInputStream(text.getBytes(UTF_8))).readAllBytes()); + // when + final byte[] actual = bufferedInputStream.readAllBytes(); + + log.info("===========> actual = {}", actual); + + // then assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); } @@ -198,16 +260,25 @@ class InputStreamReader_학습_테스트 { */ @Test void BufferedReader를_사용하여_문자열을_읽어온다() { + // given final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); - final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); - final StringBuilder actual = new StringBuilder(); + // when + final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes(UTF_16)); + final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, UTF_16)); + + final StringBuilder actual = new StringBuilder();; + final List readLines = br.lines().collect(Collectors.toList()); + for (String readLine : readLines) { + actual.append(readLine).append("\r\n"); + } - assertThat(actual).hasToString(emoji); + // then + assertThat(actual.toString()).contains(emoji); } } } diff --git a/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java b/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java new file mode 100644 index 0000000000..fe965ff0a1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/CharacterSet.java @@ -0,0 +1,18 @@ +package org.apache.coyote.common; + +public enum CharacterSet { + + UTF_8("utf-8"); + + private static final String CHARACTER_SET = "charset"; + + private final String source; + + CharacterSet(final String source) { + this.source = source; + } + + public String source() { + return CHARACTER_SET + "=" + source; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java b/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java new file mode 100644 index 0000000000..0555ca08b1 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/HeaderType.java @@ -0,0 +1,23 @@ +package org.apache.coyote.common; + +public enum HeaderType { + + ACCEPT("Accept"), + CONTENT_TYPE("Content-Type"), + CONTENT_LENGTH("Content-Length"), + HOST("Host"), + CONNECTION("Connection"), + LOCATION("Location"), + COOKIE("Cookie"), + SET_COOKIE("Set-Cookie"); + + private final String source; + + HeaderType(final String source) { + this.source = source; + } + + public String source() { + return source; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/common/Headers.java b/tomcat/src/main/java/org/apache/coyote/common/Headers.java new file mode 100644 index 0000000000..0b1c574a77 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/Headers.java @@ -0,0 +1,84 @@ +package org.apache.coyote.common; + +import org.apache.coyote.session.Cookies; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.apache.coyote.common.HeaderType.CONTENT_LENGTH; +import static org.apache.coyote.common.HeaderType.CONTENT_TYPE; +import static org.apache.coyote.common.HeaderType.LOCATION; +import static org.apache.coyote.common.HeaderType.SET_COOKIE; + +public class Headers { + + private final Map mapping = new HashMap<>(); + + public Headers() { + } + + public Headers(final Map headers) { + mapping.putAll(headers); + } + + public String getHeaderValue(final String name) { + return mapping.getOrDefault(name, null); + } + + public void addHeader(final String headerName, final String value) { + mapping.put(headerName, value); + } + + public void setContentType(final String contentType) { + mapping.put(CONTENT_TYPE.source(), contentType); + } + + public void setContentLength(final int contentLength) { + mapping.put(CONTENT_LENGTH.source(), String.valueOf(contentLength)); + } + + public void setLocation(final String location) { + mapping.put(LOCATION.source(), location); + } + + public void setCookies(final Cookies cookies) { + final String cookieValues = cookies.cookieNames() + .stream() + .map(cookieName -> cookieName + "=" + cookies.getCookieValue(cookieName)) + .collect(Collectors.joining(";")); + + mapping.put(SET_COOKIE.source(), cookieValues); + } + + public void addCookie(final String cookieName, final String cookieValue) { + final String oldSetCookies = mapping.getOrDefault(SET_COOKIE.source(), null); + if (Objects.isNull(oldSetCookies)) { + mapping.put(SET_COOKIE.source(), cookieName + "=" + cookieValue); + } + + mapping.put(SET_COOKIE.source(), oldSetCookies + ";" + cookieName + "=" + cookieValue); + } + + public List headerNames() { + return mapping.keySet() + .stream() + .sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + final String mappingResult = mapping.keySet() + .stream() + .map(headerName -> " " + headerName + " : " + mapping.get(headerName)) + .collect(Collectors.joining("," + System.lineSeparator())); + + return "Headers{" + System.lineSeparator() + + mappingResult + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java b/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java new file mode 100644 index 0000000000..39eb9873f4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/HttpHeaders.java @@ -0,0 +1,93 @@ +package org.apache.coyote.common; + +import org.apache.coyote.session.Cookies; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.apache.coyote.common.HeaderType.*; + +public class HttpHeaders { + + private static final String HEADER_DELIMITER = ":"; + private static final int HEADER_NAME_INDEX = 0; + private static final int HEADER_VALUE_INDEX = 1; + + private final Headers headers; + private final Cookies cookies; + private final Session session; + + private HttpHeaders(final Headers headers, final Cookies cookies, final Session session) { + this.headers = headers; + this.cookies = cookies; + this.session = session; + } + + public static HttpHeaders from(final List headersWithValue) { + final Map headerMapping = collectHeaderMapping(headersWithValue); + final Cookies newCookies = getCookiesBy(headerMapping); + final Session foundSession = getSessionBy(newCookies); + + return new HttpHeaders(new Headers(headerMapping), newCookies, foundSession); + } + + private static Map collectHeaderMapping(final List headersWithValue) { + return headersWithValue.stream() + .map(headerWithValue -> headerWithValue.split(HEADER_DELIMITER)) + .collect(Collectors.toMap( + entry -> entry[HEADER_NAME_INDEX].trim(), + entry -> entry[HEADER_VALUE_INDEX].trim() + )); + } + + private static Cookies getCookiesBy(final Map headers) { + final String cookiesWithValue = headers.getOrDefault(COOKIE.source(), null); + if (Objects.isNull(COOKIE.source())) { + return Cookies.empty(); + } + + return Cookies.from(cookiesWithValue); + } + + private static Session getSessionBy(final Cookies newCookies) { + final String sessionId = newCookies.getCookieValue("JSESSIONID"); + if (Objects.isNull(sessionId)) { + return Session.empty(); + } + + return SessionManager.findSession(sessionId); + } + + public String getHeaderValue(final String headerName) { + return headers.getHeaderValue(headerName); + } + + public String getCookieValue(final String cookieName) { + return cookies.getCookieValue(cookieName); + } + + public Headers headers() { + return headers; + } + + public Cookies cookies() { + return cookies; + } + + public Session session() { + return session; + } + + @Override + public String toString() { + return "HttpHeaders{" + + "headers=" + headers + System.lineSeparator() + + " " + cookies + System.lineSeparator() + + " " + session + System.lineSeparator() + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java b/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java new file mode 100644 index 0000000000..455272a38d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/HttpVersion.java @@ -0,0 +1,32 @@ +package org.apache.coyote.common; + +import java.util.Arrays; + +public enum HttpVersion { + + HTTP_1_1("HTTP/1.1"); + + private final String source; + + HttpVersion(final String source) { + this.source = source; + } + + public static HttpVersion from(final String source) { + return Arrays.stream(HttpVersion.values()) + .filter(httpVersion -> httpVersion.source.equals(source)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("입력된 값으로 HttpVersion을 조회할 수 없습니다.")); + } + + public String version() { + return source; + } + + @Override + public String toString() { + return "HttpVersion{" + + "source='" + source + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/common/MediaType.java b/tomcat/src/main/java/org/apache/coyote/common/MediaType.java new file mode 100644 index 0000000000..fc067b2886 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/common/MediaType.java @@ -0,0 +1,34 @@ +package org.apache.coyote.common; + +import java.util.Arrays; + +public enum MediaType { + + TEXT_HTML("text/html", ".html"), + TEXT_CSS("text/css", ".css"), + APPLICATION_JAVASCRIPT("application/javascript", ".js"); + + private final String source; + private final String type; + + MediaType(final String source, final String type) { + this.source = source; + this.type = type; + } + + public static MediaType from(final String resourceUrl) { + return Arrays.stream(MediaType.values()) + .filter(mediaType -> resourceUrl.endsWith(mediaType.type)) + .findFirst() + .orElse(TEXT_HTML); + } + + public String source() { + return source; + } + + @Override + public String toString() { + return source; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java new file mode 100644 index 0000000000..a2913345f5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.exception; + +public abstract class CoyoteException extends RuntimeException { + + protected CoyoteException(final String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java new file mode 100644 index 0000000000..a22fa13a2e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteHttpException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.exception; + +public class CoyoteHttpException extends CoyoteException { + + public CoyoteHttpException(final String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java new file mode 100644 index 0000000000..5e5510c298 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/exception/CoyoteIOException.java @@ -0,0 +1,8 @@ +package org.apache.coyote.exception; + +public class CoyoteIOException extends CoyoteException { + + public CoyoteIOException(final String message) { + super(message); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java new file mode 100644 index 0000000000..bef97d716d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/HomeRequestHandler.java @@ -0,0 +1,27 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.response.ResponseBody; + +import static org.apache.coyote.common.HttpVersion.HTTP_1_1; +import static org.apache.coyote.common.MediaType.TEXT_HTML; +import static org.apache.coyote.response.HttpStatus.OK; + +public class HomeRequestHandler implements RequestHandler { + + private static final String DEFAULT = "Hello world!"; + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + final ResponseBody responseBody = new ResponseBody(DEFAULT); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setHttpStatus(OK) + .setContentType(TEXT_HTML.source()) + .setContentLength(responseBody.length()) + .setResponseBody(responseBody) + .build(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java b/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java new file mode 100644 index 0000000000..2e8183d81a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/MappingInfo.java @@ -0,0 +1,36 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.request.HttpRequest; + +import java.util.Objects; + +public class MappingInfo { + + private final String httpMethod; + private final String requestPath; + + public MappingInfo(final String httpMethod, final String requestPath) { + this.httpMethod = httpMethod; + this.requestPath = requestPath; + } + + public static MappingInfo from(final HttpRequest request) { + return new MappingInfo( + request.requestLine().httpMethod().name(), + request.requestLine().requestPath().source() + ); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final MappingInfo that = (MappingInfo) o; + return Objects.equals(httpMethod, that.httpMethod) && Objects.equals(requestPath, that.requestPath); + } + + @Override + public int hashCode() { + return Objects.hash(httpMethod, requestPath); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java new file mode 100644 index 0000000000..4abb60b683 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandler.java @@ -0,0 +1,9 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; + +public interface RequestHandler { + + HttpResponse handle(final HttpRequest httpRequest); +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java new file mode 100644 index 0000000000..d9a71f3bf3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/RequestHandlerComposite.java @@ -0,0 +1,34 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.handler.get.UserLoginRequestGetHandler; +import org.apache.coyote.handler.get.UserRegisterRequestGetHandler; +import org.apache.coyote.handler.post.UserRegisterRequestPostHandler; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.coyote.request.HttpMethod.GET; +import static org.apache.coyote.request.HttpMethod.POST; + +public class RequestHandlerComposite { + + private RequestHandlerComposite() { + } + + private static final Map mapping = new HashMap<>(); + + static { + mapping.put(new MappingInfo(GET.name(), "/"), new HomeRequestHandler()); + mapping.put(new MappingInfo(GET.name(), "/login"), new UserLoginRequestGetHandler()); + mapping.put(new MappingInfo(GET.name(), "/register"), new UserRegisterRequestGetHandler()); + mapping.put(new MappingInfo(POST.name(), "/register"), new UserRegisterRequestPostHandler()); + } + + public static HttpResponse handle(final HttpRequest httpRequest) { + final RequestHandler requestHandler = mapping.getOrDefault(MappingInfo.from(httpRequest), new ResourceRequestHandler()); + + return requestHandler.handle(httpRequest); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java new file mode 100644 index 0000000000..dae94946c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/ResourceRequestHandler.java @@ -0,0 +1,28 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.common.MediaType; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.response.ResponseBody; +import org.apache.coyote.util.ResourceReader; + +import static org.apache.coyote.common.HttpVersion.HTTP_1_1; +import static org.apache.coyote.response.HttpStatus.OK; + +public class ResourceRequestHandler implements RequestHandler { + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + final String requestPath = httpRequest.requestLine().requestPath().source(); + final String resourceBody = ResourceReader.read(requestPath); + final ResponseBody responseBody = new ResponseBody(resourceBody); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setContentType(MediaType.from(requestPath).source()) + .setHttpStatus(OK) + .setResponseBody(responseBody) + .setContentLength(resourceBody.length()) + .build(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java new file mode 100644 index 0000000000..80753cde01 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/get/UserLoginRequestGetHandler.java @@ -0,0 +1,102 @@ +package org.apache.coyote.handler.get; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.handler.RequestHandler; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.request.QueryParams; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.response.ResponseBody; +import org.apache.coyote.session.Cookies; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; +import org.apache.coyote.util.ResourceReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static org.apache.coyote.common.HttpVersion.HTTP_1_1; +import static org.apache.coyote.common.MediaType.TEXT_HTML; +import static org.apache.coyote.response.HttpStatus.FOUND; +import static org.apache.coyote.response.HttpStatus.OK; + +public class UserLoginRequestGetHandler implements RequestHandler { + + private static final String LOGIN_PAGE_URI = "/login.html"; + private static final String LOGIN_SUCCESS_REDIRECT_URI = "/index.html"; + private static final String LOGIN_FAIL_REDIRECT_URI = "/401.html"; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + if (isAuthenticated(httpRequest)) { + return redirectHomePageWhenAuthenticated(); + } + + final QueryParams queryParams = httpRequest.requestLine().queryParams(); + if (queryParams.isEmpty()) { + return responseLoginPage(); + } + + final String account = queryParams.getParamValue("account"); + final String password = queryParams.getParamValue("password"); + final Optional maybeUser = InMemoryUserRepository.findByAccount(account); + if (maybeUser.isPresent() && maybeUser.get().checkPassword(password)) { + log.info("===========> 로그인된 유저 = {}", maybeUser.get()); + return redirectHomePageWhenNewAuthentication(maybeUser.get()); + } + + return redirect401PageWhenFailed(); + } + + private boolean isAuthenticated(final HttpRequest httpRequest) { + final Session foundSession = SessionManager.findSession(httpRequest.getCookieValue("JSESSIONID")); + + return Objects.nonNull(foundSession); + } + + private HttpResponse redirectHomePageWhenAuthenticated() { + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setHttpStatus(FOUND) + .sendRedirect(LOGIN_SUCCESS_REDIRECT_URI) + .build(); + } + + private HttpResponse responseLoginPage() { + final String loginPageResource = ResourceReader.read(LOGIN_PAGE_URI); + final ResponseBody loginPageBody = new ResponseBody(loginPageResource); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setHttpStatus(OK) + .setContentType(TEXT_HTML.source()) + .setContentLength(loginPageBody.length()) + .setResponseBody(loginPageBody) + .build(); + } + + private HttpResponse redirectHomePageWhenNewAuthentication(final User loginUser) { + final Session newSession = new Session(UUID.randomUUID().toString()); + newSession.setAttribute("account", loginUser.getAccount()); + SessionManager.add(newSession); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setCookies(Cookies.ofJSessionId(newSession.id())) + .sendRedirect(LOGIN_SUCCESS_REDIRECT_URI) + .build(); + } + + private HttpResponse redirect401PageWhenFailed() { + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setHttpStatus(FOUND) + .sendRedirect(LOGIN_FAIL_REDIRECT_URI) + .build(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java new file mode 100644 index 0000000000..78505342bc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandler.java @@ -0,0 +1,30 @@ +package org.apache.coyote.handler.get; + +import org.apache.coyote.handler.RequestHandler; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.response.ResponseBody; +import org.apache.coyote.util.ResourceReader; + +import static org.apache.coyote.common.HttpVersion.HTTP_1_1; +import static org.apache.coyote.common.MediaType.TEXT_HTML; +import static org.apache.coyote.response.HttpStatus.OK; + +public class UserRegisterRequestGetHandler implements RequestHandler { + + private static final String REGISTER_PAGE_URI = "/register.html"; + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + final String registerPageResource = ResourceReader.read(REGISTER_PAGE_URI); + final ResponseBody registerPageBody = new ResponseBody(registerPageResource); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setHttpStatus(OK) + .setContentType(TEXT_HTML.source()) + .setContentLength(registerPageBody.length()) + .setResponseBody(registerPageBody) + .build(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java b/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java new file mode 100644 index 0000000000..7f765afb16 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandler.java @@ -0,0 +1,40 @@ +package org.apache.coyote.handler.post; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.handler.RequestHandler; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.request.RequestBody; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.session.Cookies; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; + +import java.util.UUID; + +import static org.apache.coyote.common.HttpVersion.HTTP_1_1; + +public class UserRegisterRequestPostHandler implements RequestHandler { + + private static final String REGISTER_SUCCESS_REDIRECT_URI = "/index.html"; + + @Override + public HttpResponse handle(final HttpRequest httpRequest) { + final RequestBody requestBody = httpRequest.requestBody(); + final String account = requestBody.getBodyValue("account"); + final String password = requestBody.getBodyValue("password"); + final String email = requestBody.getBodyValue("email"); + + InMemoryUserRepository.save(new User(account, password, email)); + + final Session newSession = new Session(UUID.randomUUID().toString()); + newSession.setAttribute("account", account); + SessionManager.add(newSession); + + return HttpResponse.builder() + .setHttpVersion(HTTP_1_1) + .setCookies(Cookies.ofJSessionId(newSession.id())) + .sendRedirect(REGISTER_SUCCESS_REDIRECT_URI) + .build(); + } +} 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..98756e3a57 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -2,10 +2,17 @@ import nextstep.jwp.exception.UncheckedServletException; import org.apache.coyote.Processor; +import org.apache.coyote.request.HttpRequest; +import org.apache.coyote.response.HttpResponse; +import org.apache.coyote.handler.RequestHandlerComposite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.Socket; public class Http11Processor implements Runnable, Processor { @@ -26,19 +33,16 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try (final InputStream inputStream = connection.getInputStream(); + final OutputStream outputStream = connection.getOutputStream(); + final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));) { - final var responseBody = "Hello world!"; + final HttpRequest request = HttpRequest.from(br); + log.info("=============> HTTP Request : \n {}", request); + final HttpResponse response = RequestHandlerComposite.handle(request); + log.info("=============> HTTP Response : \n {}", response); - 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.write(Http11ResponseConverter.convertToBytes(response)); outputStream.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java new file mode 100644 index 0000000000..aca18e09e5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseConverter.java @@ -0,0 +1,48 @@ +package org.apache.coyote.http11; + +import org.apache.coyote.response.HttpResponse; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class Http11ResponseConverter { + + private static final String SPACE = " "; + private static final String ENTER = "\r\n"; + + private Http11ResponseConverter() { + } + + public static byte[] convertToBytes(final HttpResponse response) { + final StringBuilder responseByteBuilder = new StringBuilder(); + appendResponseLine(response, responseByteBuilder); + appendResponseHeaders(response, responseByteBuilder); + appendResponseBody(response, responseByteBuilder); + + return responseByteBuilder.toString().getBytes(UTF_8); + } + + private static void appendResponseLine(final HttpResponse response, final StringBuilder responseByteBuilder) { + responseByteBuilder + .append(response.httpVersion().version()).append(SPACE) + .append(response.httpStatus().statusCode()).append(SPACE) + .append(response.httpStatus().statusName()).append(SPACE) + .append(ENTER); + } + + private static void appendResponseHeaders(final HttpResponse response, final StringBuilder responseByteBuilder) { + response.httpHeaders() + .headerNames() + .forEach(headerName -> { + final String headerValue = response.httpHeaders().getHeaderValue(headerName); + responseByteBuilder + .append(headerName).append(": ").append(headerValue).append(SPACE) + .append(ENTER); + }); + } + + private static void appendResponseBody(final HttpResponse response, final StringBuilder responseByteBuilder) { + responseByteBuilder + .append(ENTER) + .append(response.responseBody().source()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java new file mode 100644 index 0000000000..c5c5ef09cb --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/HttpMethod.java @@ -0,0 +1,22 @@ +package org.apache.coyote.request; + +import java.util.Arrays; + +import static java.util.Locale.ENGLISH; + +public enum HttpMethod { + + GET, POST, PATCH, PUT, DELETE, OPTION; + + public static HttpMethod from(final String source) { + return Arrays.stream(HttpMethod.values()) + .filter(httpMethod -> httpMethod.contains(source)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 HttpMethod 값입니다.")); + } + + private boolean contains(final String source) { + return source.toUpperCase(ENGLISH) + .contains(this.name()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java new file mode 100644 index 0000000000..54d33553c3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/HttpRequest.java @@ -0,0 +1,111 @@ +package org.apache.coyote.request; + +import org.apache.coyote.common.Headers; +import org.apache.coyote.common.HttpHeaders; +import org.apache.coyote.common.MediaType; +import org.apache.coyote.exception.CoyoteIOException; +import org.apache.coyote.session.Cookies; +import org.apache.coyote.session.Session; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.apache.coyote.common.HeaderType.CONTENT_LENGTH; + +public class HttpRequest { + + private static final String HEADER_END_CONDITION = ""; + + private final RequestLine requestLine; + private final HttpHeaders httpHeaders; + private final MediaType mediaType; + private final RequestBody requestBody; + + private HttpRequest(final RequestLine requestLine, + final HttpHeaders headersV2, + final MediaType mediaType, + final RequestBody requestBody + ) { + this.requestLine = requestLine; + this.httpHeaders = headersV2; + this.mediaType = mediaType; + this.requestBody = requestBody; + } + + public static HttpRequest from(final BufferedReader br) { + try { + final RequestLine requestLine = RequestLine.from(br.readLine()); + final HttpHeaders httpHeaders = parseToHttpHeaders(br); + final MediaType mediaType = MediaType.from(requestLine.requestPath().source()); + final RequestBody requestBody = parseToResponseBody(br, httpHeaders); + + return new HttpRequest(requestLine, httpHeaders, mediaType, requestBody); + } catch (IOException e) { + throw new CoyoteIOException("HTTP 요청 정보를 읽던 도중에 예외가 발생하였습니다."); + } + } + + private static HttpHeaders parseToHttpHeaders(final BufferedReader br) throws IOException { + final List headersWithValue = new ArrayList<>(); + String header = br.readLine(); + while (!header.equals(HEADER_END_CONDITION)) { + headersWithValue.add(header); + header = br.readLine(); + } + + return HttpHeaders.from(headersWithValue); + } + + private static RequestBody parseToResponseBody(final BufferedReader br, final HttpHeaders httpHeaders) throws IOException { + final String contentLengthHeader = httpHeaders.getHeaderValue(CONTENT_LENGTH.source()); + if (Objects.isNull(contentLengthHeader)) { + return RequestBody.empty(); + } + + final int contentLength = Integer.parseInt(contentLengthHeader); + final char[] buffer = new char[contentLength]; + br.read(buffer, 0, contentLength); + return RequestBody.from(new String(buffer)); + } + + public RequestLine requestLine() { + return requestLine; + } + + public String getCookieValue(final String cookieName) { + return httpHeaders.getCookieValue(cookieName); + } + + public Headers headers() { + return httpHeaders.headers(); + } + + public Cookies cookies() { + return httpHeaders.cookies(); + } + + public Session session() { + return httpHeaders.session(); + } + + public MediaType mediaType() { + return mediaType; + } + + public RequestBody requestBody() { + return requestBody; + } + + @Override + public String toString() { + return "HttpRequest{" + System.lineSeparator() + + " requestLine = " + requestLine + ", " + System.lineSeparator() + + " httpHeaders = " + httpHeaders + System.lineSeparator() + + " mediaType = " + mediaType + System.lineSeparator() + + " requestBody = " + requestBody + System.lineSeparator() + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java b/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java new file mode 100644 index 0000000000..d34a82bcaf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/QueryParams.java @@ -0,0 +1,71 @@ +package org.apache.coyote.request; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class QueryParams { + + private static final String QUERY_PARAM_START_CONDITION = "?"; + private static final String QUERY_PARAMS_DELIMITER = "&"; + private static final String QUERY_PARAM_VALUE_DELIMITER = "="; + private static final int PARAM_NAME_INDEX = 0; + private static final int PARAM_VALUE_INDEX = 1; + + private final Map params = new HashMap<>(); + + private QueryParams(final Map params) { + this.params.putAll(params); + } + + public static QueryParams empty() { + return new QueryParams(new HashMap<>()); + } + + public static QueryParams from(final String queryParamsWithValue) { + final Map mapping = new HashMap<>(); + + final int paramStartIndex = queryParamsWithValue.indexOf(QUERY_PARAM_START_CONDITION) + 1; + final String[] queryParams = queryParamsWithValue.substring(paramStartIndex).split(QUERY_PARAMS_DELIMITER); + for (String queryParam : queryParams) { + final String[] paramWithValue = queryParam.split(QUERY_PARAM_VALUE_DELIMITER); + mapping.put(paramWithValue[PARAM_NAME_INDEX], paramWithValue[PARAM_VALUE_INDEX]); + } + + return new QueryParams(mapping); + } + + public boolean isEmpty() { + return params.isEmpty(); + } + + public List paramNames() { + return new ArrayList<>(params.keySet()); + } + + public String getParamValue(final String name) { + return params.getOrDefault(name, null); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final QueryParams that = (QueryParams) o; + return Objects.equals(params, that.params); + } + + @Override + public int hashCode() { + return Objects.hash(params); + } + + @Override + public String toString() { + return "QueryParams{" + + "params=" + params + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java new file mode 100644 index 0000000000..e13b6ee873 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java @@ -0,0 +1,54 @@ +package org.apache.coyote.request; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RequestBody { + + private static final String REQUEST_DELIMITER = "&"; + private static final String KEY_VALUE_DELIMITER = "="; + private static final int NAME_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map body; + + private RequestBody(final Map body) { + this.body = body; + } + + public static RequestBody from(final String requestBodyValue) { + if (requestBodyValue.isBlank()) { + return RequestBody.empty(); + } + + final Map mapping = Arrays.asList(requestBodyValue.split(REQUEST_DELIMITER)) + .stream() + .map(bodyEntry -> bodyEntry.split(KEY_VALUE_DELIMITER)) + .collect(Collectors.toMap(keyValue -> keyValue[NAME_INDEX], keyValue -> keyValue[VALUE_INDEX])); + + return new RequestBody(mapping); + } + + public static RequestBody empty() { + return new RequestBody(Collections.emptyMap()); + } + + public List names() { + return new ArrayList<>(body.keySet()); + } + + public String getBodyValue(final String name) { + return body.getOrDefault(name, null); + } + + @Override + public String toString() { + return "RequestBody{" + + "body=" + body + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java new file mode 100644 index 0000000000..083af834c0 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java @@ -0,0 +1,79 @@ +package org.apache.coyote.request; + +import org.apache.coyote.common.HttpVersion; +import org.apache.coyote.exception.CoyoteHttpException; + +import java.util.regex.Pattern; + +public class RequestLine { + + private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("^[^?]*\\?[^?]*$"); + private static final String REQUEST_LINE_DELIMITER = " "; + private static final String QUERY_PARAM_START_DELIMITER = "?"; + private static final int REQUEST_LINE_LENGTH = 3; + private static final int HTTP_METHOD_INDEX = 0; + private static final int HTTP_REQUEST_URI_INDEX = 1; + private static final int HTTP_VERSION_INDEX = 2; + + private final HttpMethod httpMethod; + private final HttpVersion httpVersion; + private final RequestPath requestPath; + private final QueryParams queryParams; + + private RequestLine(final HttpMethod httpMethod, final HttpVersion httpVersion, final RequestPath requestPath, final QueryParams queryParams) { + this.httpMethod = httpMethod; + this.httpVersion = httpVersion; + this.requestPath = requestPath; + this.queryParams = queryParams; + } + + public static RequestLine from(final String requestLine) { + final String[] requestLineValues = requestLine.split(REQUEST_LINE_DELIMITER); + if (requestLineValues.length != REQUEST_LINE_LENGTH) { + throw new CoyoteHttpException("HTTP 요청으로 들어온 값의 첫 번째 라인에 HttpMethod, URI, HttpVersion가 존재해야 합니다."); + } + + final HttpMethod httpMethod = HttpMethod.from(requestLineValues[HTTP_METHOD_INDEX]); + final HttpVersion httpVersion = HttpVersion.from(requestLineValues[HTTP_VERSION_INDEX]); + + final String requestUri = requestLineValues[HTTP_REQUEST_URI_INDEX]; + QueryParams queryParams = QueryParams.empty(); + RequestPath requestPath = new RequestPath(requestUri); + if (QUERY_PARAM_PATTERN.matcher(requestUri).matches()) { + final int queryParamStartIndex = requestUri.indexOf(QUERY_PARAM_START_DELIMITER); + final String requestPathValue = requestUri.substring(0, queryParamStartIndex); + final String queryParamsValue = requestUri.substring(queryParamStartIndex); + + requestPath = new RequestPath(requestPathValue); + queryParams = QueryParams.from(queryParamsValue); + } + + return new RequestLine(httpMethod, httpVersion, requestPath, queryParams); + } + + public HttpMethod httpMethod() { + return httpMethod; + } + + public HttpVersion httpVersion() { + return httpVersion; + } + + public RequestPath requestPath() { + return requestPath; + } + + public QueryParams queryParams() { + return queryParams; + } + + @Override + public String toString() { + return "RequestLine{" + + "httpMethod=" + httpMethod + + ", httpVersion=" + httpVersion + + ", requestPath=" + requestPath + + ", queryParams=" + queryParams + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java b/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java new file mode 100644 index 0000000000..f72eddd5b8 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestPath.java @@ -0,0 +1,37 @@ +package org.apache.coyote.request; + +import java.util.Objects; + +public class RequestPath { + + private final String source; + + public RequestPath(final String source) { + if (Objects.isNull(source)) { + throw new IllegalArgumentException("RequestPath 에 null 이 들어올 수 없습니다."); + } + this.source = source; + } + + public String source() { + return source; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final RequestPath that = (RequestPath) o; + return Objects.equals(source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(source); + } + + @Override + public String toString() { + return source; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java new file mode 100644 index 0000000000..f02c7579e3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/HttpResponse.java @@ -0,0 +1,105 @@ +package org.apache.coyote.response; + +import org.apache.coyote.common.Headers; +import org.apache.coyote.common.HttpVersion; +import org.apache.coyote.session.Cookies; + +import static org.apache.coyote.common.CharacterSet.UTF_8; + +public class HttpResponse { + + private final HttpVersion httpVersion; + private final HttpStatus httpStatus; + private final Headers headers; + private final ResponseBody responseBody; + + private HttpResponse(final HttpVersion httpVersion, final HttpStatus httpStatus, final Headers headers, final ResponseBody responseBody) { + this.httpVersion = httpVersion; + this.httpStatus = httpStatus; + this.headers = headers; + this.responseBody = responseBody; + } + + public static HttpResponseBuilder builder() { + return new HttpResponseBuilder(); + } + + public HttpVersion httpVersion() { + return httpVersion; + } + + public HttpStatus httpStatus() { + return httpStatus; + } + + public Headers httpHeaders() { + return headers; + } + + public ResponseBody responseBody() { + return responseBody; + } + + @Override + public String toString() { + return "HttpResponse{" + System.lineSeparator() + + " httpVersion = " + httpVersion + ", " + System.lineSeparator() + + " httpStatus = " + httpStatus + ", " + System.lineSeparator() + + " httpHeaders = " + headers + ", " + System.lineSeparator() + + " responseBody = " + responseBody + ", " + System.lineSeparator() + + '}'; + } + + public static class HttpResponseBuilder { + + private Headers headers = new Headers(); + private ResponseBody responseBody = ResponseBody.empty(); + private HttpVersion httpVersion = HttpVersion.HTTP_1_1; + private HttpStatus httpStatus; + + public HttpResponseBuilder setHttpVersion(final HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public HttpResponseBuilder setHttpStatus(final HttpStatus httpStatus) { + this.httpStatus = httpStatus; + return this; + } + + public HttpResponseBuilder setContentType(final String contentType) { + this.headers.setContentType(contentType + ";" + UTF_8.source()); + return this; + } + + public HttpResponseBuilder setContentLength(final int contentLength) { + this.headers.setContentLength(contentLength); + return this; + } + + public HttpResponseBuilder sendRedirect(final String uri) { + this.httpStatus = HttpStatus.FOUND; + this.headers.setLocation(uri); + return this; + } + + public HttpResponseBuilder setCookies(final Cookies cookies) { + this.headers.setCookies(cookies); + return this; + } + + public HttpResponseBuilder addCookie(final String cookieName, final String cookieValue) { + this.headers.addCookie(cookieName, cookieValue); + return this; + } + + public HttpResponseBuilder setResponseBody(final ResponseBody responseBody) { + this.responseBody = responseBody; + return this; + } + + public HttpResponse build() { + return new HttpResponse(httpVersion, httpStatus, headers, responseBody); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java new file mode 100644 index 0000000000..24115ef822 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/HttpStatus.java @@ -0,0 +1,31 @@ +package org.apache.coyote.response; + +public enum HttpStatus { + + OK("OK", 200), + FOUND("FOUND", 302); + + private final String statusName; + private final int statusCode; + + HttpStatus(final String statusName, final int statusCode) { + this.statusName = statusName; + this.statusCode = statusCode; + } + + public String statusName() { + return statusName; + } + + public int statusCode() { + return statusCode; + } + + @Override + public String toString() { + return "HttpStatus{" + + "statusName='" + statusName + '\'' + + ", statusCode=" + statusCode + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java b/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java new file mode 100644 index 0000000000..2966837c93 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/ResponseBody.java @@ -0,0 +1,37 @@ +package org.apache.coyote.response; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class ResponseBody { + + private static final String EMPTY_BODY = ""; + + private final String source; + + public ResponseBody(final String source) { + this.source = source; + } + + public static ResponseBody empty() { + return new ResponseBody(EMPTY_BODY); + } + + public byte[] bytes() { + return source.getBytes(UTF_8); + } + + public int length() { + return bytes().length; + } + + public String source() { + return source; + } + + @Override + public String toString() { + return "ResponseBody.Length{" + + "source='" + source.length() + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/session/Cookies.java b/tomcat/src/main/java/org/apache/coyote/session/Cookies.java new file mode 100644 index 0000000000..19e1448468 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/session/Cookies.java @@ -0,0 +1,80 @@ +package org.apache.coyote.session; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Cookies { + + private static final String COOKIE_HEADER_DELIMITER = ";"; + private static final String COOKIE_VALUE_DELIMITER = "="; + private static final int COOKIE_KEY_INDEX = 0; + private static final int COOKIE_VALUE_INDEX = 1; + + private final Map cookie; + + private Cookies(final Map cookie) { + this.cookie = cookie; + } + + public static Cookies from(final String cookieValues) { + if (Objects.isNull(cookieValues) || cookieValues.isBlank()) { + return empty(); + } + + return new Cookies(collectCookieMapping(cookieValues)); + } + + private static Map collectCookieMapping(final String cookieValues) { + return Arrays.stream(cookieValues.split(COOKIE_HEADER_DELIMITER)) + .map(cookieEntry -> cookieEntry.split(COOKIE_VALUE_DELIMITER)) + .collect(Collectors.toMap( + cookieEntry -> cookieEntry[COOKIE_KEY_INDEX].trim(), + cookieEntry -> cookieEntry[COOKIE_VALUE_INDEX].trim() + )); + } + + public static Cookies ofJSessionId(final String sessionId) { + return new Cookies(Map.of("JSESSIONID", sessionId)); + } + + public static Cookies empty() { + return new Cookies(new HashMap<>()); + } + + public void addCookie(final Cookies other) { + cookie.putAll(other.cookie); + } + + public List cookieNames() { + return new ArrayList<>(cookie.keySet()); + } + + public String getCookieValue(final String cookieName) { + return cookie.getOrDefault(cookieName, null); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Cookies cookies = (Cookies) o; + return Objects.equals(cookie, cookies.cookie); + } + + @Override + public int hashCode() { + return Objects.hash(cookie); + } + + @Override + public String toString() { + return "Cookies{" + + "cookie=" + cookie + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/session/Session.java b/tomcat/src/main/java/org/apache/coyote/session/Session.java new file mode 100644 index 0000000000..541f50e46e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/session/Session.java @@ -0,0 +1,56 @@ +package org.apache.coyote.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public static Session empty() { + return new Session(null); + } + + public Object getAttribute(final String name) { + return values.getOrDefault(name, null); + } + + public void setAttribute(final String name, final String value) { + values.put(name, value); + } + + public void removeAttribute(final String name) { + values.remove(name); + } + + public String id() { + return id; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Session session = (Session) o; + return Objects.equals(id, session.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Session{" + + "id='" + id + '\'' + + ", values=" + values + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java new file mode 100644 index 0000000000..b45ae3352e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/session/SessionManager.java @@ -0,0 +1,24 @@ +package org.apache.coyote.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + private SessionManager() { + } + + public static void add(final Session session) { + SESSIONS.put(session.id(), session); + } + + public static Session findSession(final String id) { + return SESSIONS.getOrDefault(id, null); + } + + public void remove(final Session session) { + SESSIONS.remove(session.id()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java b/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java new file mode 100644 index 0000000000..b55e001802 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/util/ResourceReader.java @@ -0,0 +1,28 @@ +package org.apache.coyote.util; + +import org.apache.coyote.exception.CoyoteIOException; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ResourceReader { + + private ResourceReader() { + } + + private static final String RESOURCE = "static"; + + public static String read(final String resourceUri) { + try { + final URL resourceUrl = ResourceReader.class.getClassLoader().getResource(RESOURCE + resourceUri); + final Path resourcePath = new File(resourceUrl.getFile()).toPath(); + + return Files.readString(resourcePath); + } catch (NullPointerException | IOException e) { + throw new CoyoteIOException("정적 파일을 읽는 도중에 오류가 발생하였습니다."); + } + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..e0d3139b7b 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -22,7 +22,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..7aed01e800 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,8 +1,9 @@ package nextstep.org.apache.coyote.http11; -import support.StubSocket; +import org.apache.coyote.Processor; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; +import support.StubSocket; import java.io.File; import java.io.IOException; @@ -16,17 +17,17 @@ class Http11ProcessorTest { @Test void process() { // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); + final StubSocket socket = new StubSocket(); + final Processor processor = new Http11Processor(socket); // when processor.process(socket); // then - var expected = String.join("\r\n", + final String expected = String.join("\r\n", "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", "Content-Length: 12 ", + "Content-Type: text/html;charset=utf-8 ", "", "Hello world!"); @@ -36,14 +37,14 @@ void process() { @Test void index() throws IOException { // given - final String httpRequest= String.join("\r\n", + 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 StubSocket socket = new StubSocket(httpRequest); final Http11Processor processor = new Http11Processor(socket); // when @@ -51,11 +52,11 @@ void index() throws IOException { // then final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + final String expected = "HTTP/1.1 200 OK \r\n" + + "Content-Length: 5518 \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected); } diff --git a/tomcat/src/test/java/org/apache/coyote/HttpFormTestUtils.java b/tomcat/src/test/java/org/apache/coyote/HttpFormTestUtils.java new file mode 100644 index 0000000000..8f8af2e20c --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/HttpFormTestUtils.java @@ -0,0 +1,109 @@ +package org.apache.coyote; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class HttpFormTestUtils { + + private final StringBuilder sb; + + private HttpFormTestUtils(final StringBuilder sb) { + this.sb = sb; + } + + public static HttpFormTestUtils builder() { + return new HttpFormTestUtils(new StringBuilder()); + } + + public HttpFormTestUtils http11() { + sb.append("HTTP/1.1 "); + return this; + } + + public HttpFormTestUtils GET() { + sb.append("GET "); + return this; + } + + public HttpFormTestUtils POST() { + sb.append("POST "); + return this; + } + + public HttpFormTestUtils requestUri(final String uri) { + sb.append(uri).append(" "); + return this; + } + + public HttpFormTestUtils OK() { + sb.append("200 OK "); + return this; + } + + public HttpFormTestUtils FOUND() { + sb.append("302 FOUND "); + return this; + } + + public HttpFormTestUtils host(final String host) { + sb.append("Host: ").append(host).append(" "); + return this; + } + + public HttpFormTestUtils connection(final String connection) { + sb.append("Connection: ").append(connection).append(" "); + return this; + } + + public HttpFormTestUtils enter() { + sb.append("\r\n"); + return this; + } + + public HttpFormTestUtils accept(final String accept) { + sb.append("Accept: ").append(accept).append(" "); + return this; + } + + public HttpFormTestUtils contentLength(final String length) { + sb.append("Content-Length: ").append(length).append(" "); + return this; + } + + public HttpFormTestUtils contentType(final String type) { + sb.append("Content-Type: ").append(type).append(" "); + return this; + } + + public HttpFormTestUtils location(final String uri) { + sb.append("Location: ").append(uri).append(" "); + return this; + } + + public HttpFormTestUtils setCookie(final String cookie) { + sb.append("Set-Cookie: ").append(cookie); + return this; + } + + public HttpFormTestUtils responseBody(final String body) { + sb.append(body); + return this; + } + + public HttpFormTestUtils responseByResource(final String name) throws IOException { + final URL resource = getClass().getClassLoader().getResource(name); + sb.append(new String(Files.readAllBytes(new File(resource.getFile()).toPath()))); + return this; + } + + public HttpFormTestUtils requestBody(final String body) { + sb.append(body); + return this; + } + + public String build() { + return sb.toString(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/common/HeadersTest.java b/tomcat/src/test/java/org/apache/coyote/common/HeadersTest.java new file mode 100644 index 0000000000..56dc761c3a --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/common/HeadersTest.java @@ -0,0 +1,56 @@ +package org.apache.coyote.common; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HeadersTest { + + @Test + void 헤더와_값이_하나의_문자열로_놓여진_상태의_목록을_이용하여_생성에_성공한다() { + // given + final Map headersWithValue = Map.of( + "Accept", "text/html;charset=utf-8", + "Connection", "keep-alive" + ); + + // expect + assertThatCode(() -> new Headers(headersWithValue)) + .doesNotThrowAnyException(); + } + + @Test + void 헤더와_값이_키_값_대칭인_상태로_놓여진_자료구조를_이용하여_생성에_성공한다() { + // given + final Map headersWithValue = Map.of( + "Accept", "text/html;charset=utf-8", + "Connection", "keep-alive" + ); + + // expect + assertThatCode(() -> new Headers(headersWithValue)) + .doesNotThrowAnyException(); + } + + @Test + void 헤더_이름_목록을_정렬하여_가져올_수_있다() { + // given + final Map headersWithValue = Map.of( + "Connection", "keep-alive", + "Accept", "text/html;charset=utf-8" + ); + + // when + final Headers headers = new Headers(headersWithValue); + + // then + assertThat(headers.headerNames()).containsExactly("Accept", "Connection"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/common/HttpHeadersTest.java b/tomcat/src/test/java/org/apache/coyote/common/HttpHeadersTest.java new file mode 100644 index 0000000000..75b28ab73f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/common/HttpHeadersTest.java @@ -0,0 +1,84 @@ +package org.apache.coyote.common; + +import org.apache.coyote.session.Cookies; +import org.apache.coyote.session.Session; +import org.apache.coyote.session.SessionManager; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpHeadersTest { + + @Test + void 헤더와_값이_하나의_문자열로_놓여진_상태의_목록을_이용하여_생성에_성공한다() { + // given + final List headersWithValue = List.of( + "Accept: text/html;charset=utf-8", + "Connection: keep-alive" + ); + + // expect + assertThatCode(() -> HttpHeaders.from(headersWithValue)) + .doesNotThrowAnyException(); + } + + @Test + void 헤더에_쿠키와_내부에_JSESSIONID가_있을_경우_쿠키와_세션에_값이_들어가도록_생성한다() { + // given + final String sessionId = UUID.randomUUID().toString(); + final Session session = new Session(sessionId); + session.setAttribute("name", "hyena"); + SessionManager.add(session); + + final List headersWithValue = List.of( + "Accept: text/html;charset=utf-8", + "Connection: keep-alive", + "Cookie: JSESSIONID=" + sessionId + ); + + // when + final HttpHeaders httpHeaders = HttpHeaders.from(headersWithValue); + + // then + final Cookies actualCookies = httpHeaders.cookies(); + final Session actualSessions = httpHeaders.session(); + + assertAll( + () -> assertThat(actualCookies.getCookieValue("JSESSIONID")).isEqualTo(sessionId), + () -> assertThat(actualSessions.getAttribute("name")).isEqualTo("hyena") + ); + } + + @Test + void 헤더에_쿠키가_없을_경우에_빈_쿠키를_반환한다() { + // given + final HttpHeaders httpHeaders = HttpHeaders.from(List.of( + "Accept: text/html;charset=utf-8", + "Connection: keep-alive" + )); + + // expect + assertThat(httpHeaders.cookies()).isEqualTo(Cookies.empty()); + } + + @Test + void 헤더에_세션이_없을_경우에_세션을_반환한다() { + // given + final HttpHeaders httpHeaders = HttpHeaders.from(List.of( + "Accept: text/html;charset=utf-8", + "Connection: keep-alive" + )); + + // expect + assertThat(httpHeaders.session()).isEqualTo(Session.empty()); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/common/HttpRequestTest.java b/tomcat/src/test/java/org/apache/coyote/common/HttpRequestTest.java new file mode 100644 index 0000000000..19170af422 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/common/HttpRequestTest.java @@ -0,0 +1,71 @@ +package org.apache.coyote.common; + +import org.apache.coyote.exception.CoyoteHttpException; +import org.apache.coyote.request.HttpRequest; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpRequestTest { + + @Test + void BufferedReader를_이용해서_HTTP_요청의_첫_번째_라인에_HTTP버전과_URI와_HTTP메서드가_존재한다면_생성에_성공한다() { + // given + final byte[] requestFirstLine = "GET /index.html HTTP/1.1\r\n\r\n ".getBytes(UTF_8); + final BufferedReader requestReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestFirstLine))); + + // when + final HttpRequest httpRequest = HttpRequest.from(requestReader); + + // then + assertAll( + () -> assertThat(httpRequest.requestLine().httpMethod().name()).isEqualTo("GET"), + () -> assertThat(httpRequest.requestLine().requestPath().source()).isEqualTo("/index.html"), + () -> assertThat(httpRequest.requestLine().httpVersion().version()).isEqualTo("HTTP/1.1") + ); + } + + @Test + void BufferedReader를_이용해서_HTTP_요청의_첫_번째_라인에_HTTP버전과_URI와_HTTP메서드_중에_HTTP버전이_존재하지_않는다면_예외가_발생한다() { + // given + final byte[] requestFirstLine = "GET /index.html\r\n\r\n ".getBytes(UTF_8); + final BufferedReader requestReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestFirstLine))); + + // expect + assertThatThrownBy(() -> HttpRequest.from(requestReader)) + .isInstanceOf(CoyoteHttpException.class); + } + + @Test + void BufferedReader를_이용해서_HTTP_요청의_첫_번째_라인에_HTTP버전과_URI와_HTTP메서드_중에_URI가_존재하지_않는다면_예외가_발생한다() { + // given + final byte[] requestFirstLine = "GET HTTP/1.1\r\n\r\n ".getBytes(UTF_8); + final BufferedReader requestReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestFirstLine))); + + // expect + assertThatThrownBy(() -> HttpRequest.from(requestReader)) + .isInstanceOf(CoyoteHttpException.class); + } + + @Test + void BufferedReader를_이용해서_HTTP_요청의_첫_번째_라인에_HTTP버전과_URI와_HTTP메서드_중에_HTTP메서드가_존재하지_않는다면_예외가_발생한다() { + // given + final byte[] requestFirstLine = "/index.html HTTP/1.1\r\n\r\n ".getBytes(UTF_8); + final BufferedReader requestReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(requestFirstLine))); + + // expect + assertThatThrownBy(() -> HttpRequest.from(requestReader)) + .isInstanceOf(CoyoteHttpException.class); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/common/HttpVersionTest.java b/tomcat/src/test/java/org/apache/coyote/common/HttpVersionTest.java new file mode 100644 index 0000000000..986de1d78c --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/common/HttpVersionTest.java @@ -0,0 +1,24 @@ +package org.apache.coyote.common; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpVersionTest { + + @Test + void HTTP_버전을_입력된_문자열을_통해서_조회할_수_있다() { + // given + final String inputSource = "HTTP/1.1"; + + // when + final HttpVersion foundHttpVersion = HttpVersion.from(inputSource); + + // then + assertThat(foundHttpVersion).isEqualTo(HttpVersion.HTTP_1_1); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/HomeRequestHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/HomeRequestHandlerTest.java new file mode 100644 index 0000000000..ba6370cf03 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/HomeRequestHandlerTest.java @@ -0,0 +1,45 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.HttpFormTestUtils; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HomeRequestHandlerTest { + + @Test + void HelloWorld_페이지를_응답한다() { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("text/html;charset=utf-8").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("12").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseBody("Hello world!") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/MappingInfoTest.java b/tomcat/src/test/java/org/apache/coyote/handler/MappingInfoTest.java new file mode 100644 index 0000000000..3ba88afa13 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/MappingInfoTest.java @@ -0,0 +1,39 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.request.HttpMethod; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class MappingInfoTest { + + @Test + void HttpMethod와_RequestPath를_이용하여_생성한다() { + // given + final String httpMethodValue = HttpMethod.GET.name(); + final String requestPath = "/index.html"; + + // expect + assertThatCode(() -> new MappingInfo(httpMethodValue, requestPath)) + .doesNotThrowAnyException(); + } + + @Test + void HttpMethod와_RequestPath_값이_같을_경우_동일성을_보장한다() { + // given + final String httpMethodValue = HttpMethod.GET.name(); + final String requestPath = "/index.html"; + + // when + final MappingInfo actual = new MappingInfo(httpMethodValue, requestPath); + final MappingInfo expected = new MappingInfo("GET", "/index.html"); + + // then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/ResourceRequestHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/ResourceRequestHandlerTest.java new file mode 100644 index 0000000000..ffd2b2889f --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/ResourceRequestHandlerTest.java @@ -0,0 +1,250 @@ +package org.apache.coyote.handler; + +import org.apache.coyote.HttpFormTestUtils; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ResourceRequestHandlerTest { + + @Test + void index_html_메인_홈페이지를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/index.html").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("text/html;charset=utf-8").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("5518").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/index.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void login_html_로그인_페이지를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/login.html").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("text/html;charset=utf-8").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("3716").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/login.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void register_html_회원가입_페이지를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/register.html").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("text/html;charset=utf-8").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("4203").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/register.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void 권한_오류_401_페이지_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/401.html").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("text/html;charset=utf-8").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("2426").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/401.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void chart_area_js를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/assets/chart-area.js").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("application/javascript").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("1530").enter() + .contentType("application/javascript;charset=utf-8").enter() + .enter().responseByResource("static/assets/chart-area.js") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void chart_bar_js를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/assets/chart-bar.js").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("application/javascript").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("1112").enter() + .contentType("application/javascript;charset=utf-8").enter() + .enter().responseByResource("static/assets/chart-bar.js") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void chart_pie_js를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/assets/chart-pie.js").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("application/javascript").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("597").enter() + .contentType("application/javascript;charset=utf-8").enter() + .enter().responseByResource("static/assets/chart-pie.js") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void scripts_js를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/js/scripts.js").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("0").enter() + .accept("application/javascript").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("976").enter() + .contentType("application/javascript;charset=utf-8").enter() + .enter().responseByResource("static/js/scripts.js") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/get/UserLoginRequestGetHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/get/UserLoginRequestGetHandlerTest.java new file mode 100644 index 0000000000..c25ddc2caa --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/get/UserLoginRequestGetHandlerTest.java @@ -0,0 +1,97 @@ +package org.apache.coyote.handler.get; + +import org.apache.coyote.HttpFormTestUtils; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UserLoginRequestGetHandlerTest { + + @Test + void 파일_확장자를_알_수_없고_쿼리_파라미터가_비어있을_경우에_로그인_페이지를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/login").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("3796").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/login.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } + + @Test + void 쿼리_파라미터를_이용해서_로그인을_성공했을_경우_메인_홈페이지로_리다이렉트한다() { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/login?account=gugu&password=password").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().FOUND().enter() + .location("/index.html").enter() + .setCookie("JSESSIONID=") + .build(); + + assertThat(socket.output()).contains(httpResponse); + } + + @Test + void 쿼리_파라미터를_이용해서_로그인을_실패했을_경우_401페이지로_리다이렉트한다() { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/login?account=sfgd&password=password").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().FOUND().enter() + .location("/401.html").enter() + .enter() + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandlerTest.java new file mode 100644 index 0000000000..73a6105a42 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/get/UserRegisterRequestGetHandlerTest.java @@ -0,0 +1,45 @@ +package org.apache.coyote.handler.get; + +import org.apache.coyote.HttpFormTestUtils; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UserRegisterRequestGetHandlerTest { + + @Test + void 파일_확장자를_알_수_없으면서_GET일_경우_회원가입_페이지를_응답한다() throws IOException { + // given + final String httpRequest = HttpFormTestUtils.builder() + .GET().requestUri("/register").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .enter() + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().OK().enter() + .contentLength("4319").enter() + .contentType("text/html;charset=utf-8").enter() + .enter().responseByResource("static/register.html") + .build(); + + assertThat(socket.output()).isEqualTo(httpResponse); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandlerTest.java b/tomcat/src/test/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandlerTest.java new file mode 100644 index 0000000000..fc154c7c91 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/handler/post/UserRegisterRequestPostHandlerTest.java @@ -0,0 +1,46 @@ +package org.apache.coyote.handler.post; + +import org.apache.coyote.HttpFormTestUtils; +import org.apache.coyote.Processor; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import support.StubSocket; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UserRegisterRequestPostHandlerTest { + + @Test + void 요청_바디에_회원_정보를_넣어_회원가입_요청을_성공하면_메인_홈페이지로_리다이렉트한다() { + // given + final String httpRequest = HttpFormTestUtils.builder() + .POST().requestUri("/register").http11().enter() + .host("localhost:8080").enter() + .connection("keep-alive").enter() + .contentLength("80").enter() + .contentType("application/x-www-form-urlencoded").enter() + .accept("*/*").enter() + .enter() + .requestBody("account=hyena&email=test@test.com&password=test4312") + .build(); + + final StubSocket socket = new StubSocket(httpRequest); + final Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final String httpResponse = HttpFormTestUtils.builder() + .http11().FOUND().enter() + .location("/index.html").enter() + .setCookie("JSESSIONID=") + .build(); + + assertThat(socket.output()).contains(httpResponse); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/request/QueryParamsTest.java b/tomcat/src/test/java/org/apache/coyote/request/QueryParamsTest.java new file mode 100644 index 0000000000..ea2d2b85e2 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/request/QueryParamsTest.java @@ -0,0 +1,75 @@ +package org.apache.coyote.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class QueryParamsTest { + + @Test + void URI에_쿼리_파라미터를_이용해서_생성에_성공한다() { + // given + final String uri = "/index.html?name=헤나&gender=male"; + + // expect + assertThatCode(() -> QueryParams.from(uri)) + .doesNotThrowAnyException(); + } + + @Test + void 쿼리_파라미터를_이용해서_생성에_성공한다() { + // given + final String queryParamsValue = "name=헤나&gender=male"; + + // expect + assertThatCode(() -> QueryParams.from(queryParamsValue)) + .doesNotThrowAnyException(); + } + + @Test + void URI를_이용한_생성과_쿼리_파라미터를_이용한_생성은_동일하다() { + // given + final String uri = "/index.html?name=헤나&gender=male"; + final String queryParamsValue = "name=헤나&gender=male"; + + // when + final QueryParams queryParamsByUri = QueryParams.from(uri); + final QueryParams queryParamsByValue = QueryParams.from(queryParamsValue); + + // expect + assertThat(queryParamsByUri).isEqualTo(queryParamsByValue); + } + + @Test + void 쿼리_파라미터의_키값_목록을_가져온다() { + // given + final String uri = "/index.html?name=헤나&gender=male"; + + // when + final QueryParams actual = QueryParams.from(uri); + + // then + assertThat(actual.paramNames()).contains("name", "gender"); + } + + @Test + void 쿼리_파라미터의_키값에_매칭되는_값을_가져온다() { + // given + final String uri = "/index.html?name=헤나&gender=male"; + + // when + final QueryParams actual = QueryParams.from(uri); + + // then + assertAll( + () -> assertThat(actual.getParamValue("name")).contains("헤나"), + () -> assertThat(actual.getParamValue("gender")).contains("male") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/request/RequestBodyTest.java b/tomcat/src/test/java/org/apache/coyote/request/RequestBodyTest.java new file mode 100644 index 0000000000..3476da96bc --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/request/RequestBodyTest.java @@ -0,0 +1,54 @@ +package org.apache.coyote.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestBodyTest { + + @Test + void 요청받은_바디값을_파싱하여_생성에_성공한다() { + // given + final String body = "name=헤나&type=BE"; + + // when + final RequestBody requestBody = RequestBody.from(body); + + // then + assertThatCode(() -> RequestBody.from(body)) + .doesNotThrowAnyException(); + } + + @Test + void 바디에_키값을_가져올_수_있다() { + // given + final String body = "name=헤나&type=BE"; + + // when + final RequestBody requestBody = RequestBody.from(body); + + // then + assertThat(requestBody.names()).containsExactly("name", "type"); + } + + @Test + void 키값을_통해서_매핑된_값을_가져올_수_있다() { + // given + final String body = "name=헤나&type=BE"; + + // when + final RequestBody requestBody = RequestBody.from(body); + + // then + assertAll( + () -> assertThat(requestBody.getBodyValue("name")).isEqualTo("헤나"), + () -> assertThat(requestBody.getBodyValue("type")).isEqualTo("BE") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/request/RequestLineTest.java b/tomcat/src/test/java/org/apache/coyote/request/RequestLineTest.java new file mode 100644 index 0000000000..f76439320b --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/request/RequestLineTest.java @@ -0,0 +1,65 @@ +package org.apache.coyote.request; + +import org.apache.coyote.common.HttpVersion; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestLineTest { + + @Test + void HttpMethod와_URI와_HttpVersion의_값이_들어있는_요청_라인_값을_이용해서_생성에_성공한다() { + // given + final String requestLineValue = "GET /index.html HTTP/1.1"; + + // expect + assertThatCode(() -> RequestLine.from(requestLineValue)) + .doesNotThrowAnyException(); + } + + @Test + void 요청_라인의_필드값인_HttpMethod_HttpVersion_RequestPath_QueryParams를_가져올_수_있다() { + // given + final String requestLineValue = "GET /index.html HTTP/1.1"; + + // when + final RequestLine actual = RequestLine.from(requestLineValue); + + final HttpMethod expectedHttpMethod = HttpMethod.GET; + final HttpVersion expectedHttpVersion = HttpVersion.HTTP_1_1; + final RequestPath expectedRequestPath = new RequestPath("/index.html"); + final QueryParams expectedQueryParams = QueryParams.empty(); + + // then + assertAll( + () -> assertThat(actual.httpMethod()).isEqualTo(expectedHttpMethod), + () -> assertThat(actual.httpVersion()).isEqualTo(expectedHttpVersion), + () -> assertThat(actual.requestPath()).isEqualTo(expectedRequestPath), + () -> assertThat(actual.queryParams()).isEqualTo(expectedQueryParams) + ); + } + + @Test + void URI에_쿼리_파라미터가_있을_경우_요청_패스와_쿼리_파라미터를_파싱하여_생성할_수_있다() { + // given + final String requestLineValue = "GET /index.html?account=hyena HTTP/1.1"; + + // when + final RequestLine actual = RequestLine.from(requestLineValue); + + final RequestPath expectedRequestPath = new RequestPath("/index.html"); + final QueryParams expectedQueryParams = QueryParams.from("account=hyena"); + + // then + assertAll( + () -> assertThat(actual.requestPath()).isEqualTo(expectedRequestPath), + () -> assertThat(actual.queryParams()).isEqualTo(expectedQueryParams) + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/request/RequestPathTest.java b/tomcat/src/test/java/org/apache/coyote/request/RequestPathTest.java new file mode 100644 index 0000000000..6c4ef59f5e --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/request/RequestPathTest.java @@ -0,0 +1,25 @@ +package org.apache.coyote.request; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RequestPathTest { + + @Test + void 요청_패스를_생성에_성공한다() { + assertThatCode(() -> new RequestPath("/index.html")) + .doesNotThrowAnyException(); + } + + @Test + void 요청_패스를_null로_생성할_경우_예외가_발생한다() { + assertThatThrownBy(() -> new RequestPath(null)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/response/ResponseBodyTest.java b/tomcat/src/test/java/org/apache/coyote/response/ResponseBodyTest.java new file mode 100644 index 0000000000..c81372663d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/response/ResponseBodyTest.java @@ -0,0 +1,36 @@ +package org.apache.coyote.response; + +import org.apache.coyote.request.RequestBody; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ResponseBodyTest { + + @Test + void 요청_바디값을_키_값_형태로_파싱하여_생성에_성공한다() { + // given + final String requestBodyValue = "account=hyena&type=BE"; + + // when + final RequestBody requestBody = RequestBody.from(requestBodyValue); + + // then + final List actualNames = requestBody.names(); + final String actualAccountValue = requestBody.getBodyValue("account"); + final String actualTypeValue = requestBody.getBodyValue("type"); + + assertAll( + () -> assertThat(actualNames).containsExactlyInAnyOrder("account", "type"), + () -> assertThat(actualAccountValue).isEqualTo("hyena"), + () -> assertThat(actualTypeValue).isEqualTo("BE") + ); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/session/CookiesTest.java b/tomcat/src/test/java/org/apache/coyote/session/CookiesTest.java new file mode 100644 index 0000000000..99b9960b0d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/session/CookiesTest.java @@ -0,0 +1,68 @@ +package org.apache.coyote.session; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CookiesTest { + + @Test + void 쿠키_헤더값을_이용해서_생성에_성공한다() { + // given + final String cookieValues = "JSESSIONID=" + UUID.randomUUID(); + + // expect + assertThatCode(() -> Cookies.from(cookieValues)) + .doesNotThrowAnyException(); + } + + @Test + void 세션_아이디를_이용해서_쿠키_생성에_성공한다() { + // given + final String sessionId = UUID.randomUUID().toString(); + + // expect + assertThatCode(() -> Cookies.ofJSessionId(sessionId)) + .doesNotThrowAnyException(); + } + + @Test + void 쿠키에_있는_모든_키값을_가져온다() { + // given + final String cookieValues = "name=testName;title=testTitle;content=testContent"; + + // when + final Cookies cookies = Cookies.from(cookieValues); + + // then + assertThat(cookies.cookieNames()).containsExactlyInAnyOrder("name", "title", "content"); + } + + @Test + void 쿠키_키값을_이용해서_쿠키값을_가져온다() { + // given + final String cookieValues = "name=testName;title=testTitle;content=testContent"; + final Cookies cookies = Cookies.from(cookieValues); + + // when + final String name = cookies.getCookieValue("name"); + final String title = cookies.getCookieValue("title"); + final String content = cookies.getCookieValue("content"); + + // then + assertAll( + () -> assertThat(name).isEqualTo("testName"), + () -> assertThat(title).isEqualTo("testTitle"), + () -> assertThat(content).isEqualTo("testContent") + + ); + } +} diff --git a/tomcat/src/test/java/support/StubSocket.java b/tomcat/src/test/java/support/StubSocket.java index 6ba8ba5ef9..1f308f6388 100644 --- a/tomcat/src/test/java/support/StubSocket.java +++ b/tomcat/src/test/java/support/StubSocket.java @@ -23,6 +23,7 @@ public StubSocket() { this("GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n"); } + @Override public InetAddress getInetAddress() { try { return InetAddress.getLocalHost(); @@ -31,14 +32,17 @@ public InetAddress getInetAddress() { } } + @Override public int getPort() { return 8080; } + @Override public InputStream getInputStream() { - return new ByteArrayInputStream(request.getBytes()); + return new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)); } + @Override public OutputStream getOutputStream() { return new OutputStream() { @Override