diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..9fc2fd0695 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -3,7 +3,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -18,7 +24,7 @@ class FileTest { /** * resource 디렉터리 경로 찾기 - * + *

* File 객체를 생성하려면 파일의 경로를 알아야 한다. * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? @@ -27,27 +33,32 @@ class FileTest { void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; - // todo - final String actual = ""; + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + final URL resource = systemClassLoader.getResource(fileName); + + final String actual = resource.getFile(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * + *

* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + final URL resource = systemClassLoader.getResource(fileName); + // todo - final Path path = null; + final Path path = Path.of(resource.getPath()); // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(path); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..41678bd585 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,8 +1,6 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import java.io.*; @@ -12,21 +10,23 @@ /** * 자바는 스트림(Stream)으로부터 I/O를 사용한다. * 입출력(I/O)은 하나의 시스템에서 다른 시스템으로 데이터를 이동 시킬 때 사용한다. - * + *

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

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

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

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

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

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

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

* 필터는 필터 스트림, reader, writer로 나뉜다. * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. @@ -169,12 +154,12 @@ class FilterStream_학습_테스트 { * 버퍼 크기를 지정하지 않으면 버퍼의 기본 사이즈는 얼마일까? */ @Test - void 필터인_BufferedInputStream를_사용해보자() { + void 필터인_BufferedInputStream를_사용해보자() throws IOException { final String text = "필터에 연결해보자."; final InputStream inputStream = new ByteArrayInputStream(text.getBytes()); - final InputStream bufferedInputStream = null; + final InputStream bufferedInputStream = new BufferedInputStream(inputStream); - final byte[] actual = new byte[0]; + final byte[] actual = bufferedInputStream.readAllBytes(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -197,17 +182,22 @@ class InputStreamReader_학습_테스트 { * 필터인 BufferedReader를 사용하면 readLine 메서드를 사용해서 문자열(String)을 한 줄 씩 읽어올 수 있다. */ @Test - void BufferedReader를_사용하여_문자열을_읽어온다() { + void BufferedReader를_사용하여_문자열을_읽어온다() throws IOException { final String emoji = String.join("\r\n", "😀😃😄😁😆😅😂🤣🥲☺️😊", "😇🙂🙃😉😌😍🥰😘😗😙😚", "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); - - final StringBuilder actual = new StringBuilder(); - - assertThat(actual).hasToString(emoji); + try (final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));){ + StringBuilder actual = new StringBuilder(); + String line = bufferedReader.readLine(); + while (line != null) { + actual.append(line).append("\r\n"); + line = bufferedReader.readLine(); + } + assertThat(actual).hasToString(emoji); + } } } } diff --git a/tomcat/src/main/java/org/apache/catalina/Session.java b/tomcat/src/main/java/org/apache/catalina/Session.java new file mode 100644 index 0000000000..8b68d9a417 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/Session.java @@ -0,0 +1,34 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public Object getAttribute(final String name) { + return values.get(name); + } + + public void setAttribute(final String name, final Object value) { + this.values.put(name, value); + } + + public void removeAttribute(final String name) { + this.values.remove(name); + } + + public void invalidate() { + this.values.clear(); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/SessionManager.java new file mode 100644 index 0000000000..3e550f2faf --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/SessionManager.java @@ -0,0 +1,21 @@ +package org.apache.catalina; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + + private static final Map SESSIONS = new HashMap<>(); + + public void add(final Session session) { + SESSIONS.put(session.getId(), session); + } + + public Session findSession(final String id) { + return SESSIONS.get(id); + } + + public void remove(final String id) { + SESSIONS.remove(id); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java new file mode 100644 index 0000000000..7f64dbebf2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ContentType.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11; + +import java.util.Arrays; + +public enum ContentType { + TEXT_HTML("text/html", "html"), + CSS("text/css", "css"), + APPLICATION_JSON("application/json", "json"), + JAVASCRIPT("application/javascript", "js"), + ; + private static final String DEFAULT_UTF8 = "charset=utf-8"; + private static final String EXT_SEPARATOR = "."; + + private final String type; + private final String ext; + + ContentType(String type, String ext) { + this.type = type; + this.ext = ext; + } + + public static ContentType from(String fileName) { + + int index = fileName.indexOf(EXT_SEPARATOR); + if (index == -1) { + throw new IllegalArgumentException("지원하지 않는 ContentType입니다. fileName: " + fileName); + } + String ext = fileName.substring(index + 1); + return Arrays.stream(values()) + .filter(type -> type.ext.equalsIgnoreCase(ext)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("지원하지 않는 ContentType입니다. fileName: " + ext)); + } + + public String getType() { + return String.format("%s;%s", type, DEFAULT_UTF8); + } +} 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..a7b26fa902 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,16 +1,38 @@ package org.apache.coyote.http11; +import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; +import nextstep.jwp.model.User; +import org.apache.catalina.Session; +import org.apache.catalina.SessionManager; import org.apache.coyote.Processor; +import org.apache.coyote.requests.HttpCookie; +import org.apache.coyote.requests.RequestBody; +import org.apache.coyote.requests.RequestHeader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.net.Socket; +import java.util.Arrays; +import java.util.Map; +import java.util.StringJoiner; + +import static org.apache.coyote.http11.ContentType.TEXT_HTML; public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final SessionManager sessionManager = new SessionManager(); + private static final String HTTP_11 = "HTTP/1.1"; + private static final String INDEX_URI = "/index"; + private static final String REGISTER_URI = "/register"; + private static final String LOGIN_URI = "/login"; + private static final String UNAUTHORIZED_URI = "/401.html"; private final Socket connection; @@ -27,21 +49,198 @@ public void run() { @Override public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + final var outputStream = connection.getOutputStream(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream))) { + String line = bufferedReader.readLine(); + if (line == null) { + return; + } + + String httpMethod = Arrays.stream(line.split(" ")) + .findFirst() + .orElseGet(() -> ""); - final var responseBody = "Hello world!"; + String uri = Arrays.stream(line.split(" ")) + .filter(it -> it.startsWith("/")) + .findAny() + .orElseGet(() -> ""); - 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); + RequestHeader requestHeader = readHeader(bufferedReader); + RequestBody requestBody = readBody(bufferedReader, requestHeader); - outputStream.write(response.getBytes()); - outputStream.flush(); + HttpResponse httpResponse = routeByHttpMethod(uri, httpMethod, requestHeader, requestBody); + String header = createHeader(httpResponse); + + bufferedWriter.write(header + httpResponse.getBody()); + bufferedWriter.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpResponse routeByHttpMethod(String uri, String httpMethod, RequestHeader requestHeader, RequestBody requestBody) throws IOException { + if (httpMethod.equals("POST")) { + return routePost(uri, requestHeader, requestBody); + } + + return createHttpResponse(uri, requestHeader); + } + + private HttpResponse routePost(String uri, RequestHeader requestHeader, RequestBody requestBody) throws IOException { + if (REGISTER_URI.equals(uri)) { + return doRegister(requestBody); + } + + if (LOGIN_URI.equals(uri)) { + return doLogin(requestHeader, requestBody); + } + return createHttpResponse(uri, requestHeader); + } + + private RequestHeader readHeader(BufferedReader bufferedReader) throws IOException { + final StringBuilder stringBuilder = new StringBuilder(); + for (String line = bufferedReader.readLine(); + !"".equals(line); line = bufferedReader.readLine()) { + stringBuilder.append(line).append("\r\n"); + } + return RequestHeader.from(stringBuilder.toString()); + } + + private RequestBody readBody(BufferedReader bufferedReader, RequestHeader requestHeader) throws IOException { + final String contentLength = requestHeader.get("Content-Length"); + if (contentLength == null) { + return new RequestBody(); + } + final int length = Integer.parseInt(contentLength); + char[] buffer = new char[length]; + bufferedReader.read(buffer, 0, length); + return new RequestBody(new String(buffer)); + } + + private HttpResponse createHttpResponse(String uri, RequestHeader requestHeader) throws IOException { + String path = uri; + int index = uri.indexOf("?"); + if (index != -1) { + path = uri.substring(0, index); + } + + HttpCookie cookie = HttpCookie.from(requestHeader.get("Cookie")); + if (LOGIN_URI.equals(path) + && cookie != null + && sessionManager.findSession(cookie.getJSessionId(false)) != null) { + return ViewResolver.resolveView(INDEX_URI); + } + return ViewResolver.routePath(path); + } + + private String createHeader(HttpResponse response) { + StringJoiner stringJoiner = new StringJoiner("\r\n"); + HttpStatus httpStatus = response.getHttpStatus(); + + stringJoiner.add(String.format("%s %d %s ", HTTP_11, httpStatus.getCode(), httpStatus.getMessage())); + + for (Map.Entry entry : response.getHeaders().entrySet()) { + stringJoiner.add(String.format("%s: %s ", entry.getKey(), entry.getValue())); + } + + if (httpStatus.equals(HttpStatus.FOUND)) { + stringJoiner.add(toHeaderFormat("Location", response.getBody())); + stringJoiner.add("\r\n"); + return stringJoiner.toString(); + } + stringJoiner.add(toHeaderFormat("Content-Type", response.getContentType())); + stringJoiner.add(String.format("%s %s ", "Content-Length:", response.getBody().getBytes().length)); + stringJoiner.add("\r\n"); + return stringJoiner.toString(); + } + + private String toHeaderFormat(String name, String value) { + return String.format("%s: %s ", name, value); + } + + private HttpResponse doLogin(RequestHeader requestHeader, RequestBody requestBody) throws IOException { + String query = requestBody.getItem(); + if (query.isBlank()) { + return new HttpResponse(LOGIN_URI, HttpStatus.FOUND, TEXT_HTML); + } + + String account = ""; + String password = ""; + + for (String parameter : query.split("&")) { + int idx = parameter.indexOf("="); + String key = parameter.substring(0, idx); + String value = parameter.substring(idx + 1); + if ("account".equals(key)) { + account = value; + } + if ("password".equals(key)) { + password = value; + } + } + + User user = InMemoryUserRepository.findByAccount(account) + .orElse(null); + + if (user != null && user.checkPassword(password)) { + HttpCookie cookie = HttpCookie.from(requestHeader.get("Cookie")); + HttpResponse httpResponse = new HttpResponse(INDEX_URI, HttpStatus.FOUND, TEXT_HTML); + + if (cookie.getJSessionId(false) == null) { + String jSessionId = cookie.getJSessionId(true); + httpResponse.addHeader("Set-Cookie", "JSESSIONID=" + jSessionId); + Session session = new Session(jSessionId); + session.setAttribute("user", user); + sessionManager.add(session); + } + + if (log.isInfoEnabled()) { + log.info(String.format("%s %s", "로그인 성공!", user)); + } + return httpResponse; + } + + return ViewResolver.resolveView(UNAUTHORIZED_URI); + } + + private HttpResponse doRegister(RequestBody requestBody) throws IOException { + String query = requestBody.getItem(); + if (query.isBlank()) { + return ViewResolver.resolveView(REGISTER_URI); + } + + String account = ""; + String password = ""; + String email = ""; + + for (String parameter : query.split("&")) { + int idx = parameter.indexOf("="); + String key = parameter.substring(0, idx); + String value = parameter.substring(idx + 1); + + if ("account".equals(key)) { + account = value; + } + if ("password".equals(key)) { + password = value; + } + if ("email".equals(key)) { + email = value; + } + } + + if (account.isBlank() || password.isBlank() || email.isBlank()) { + return ViewResolver.resolveView(REGISTER_URI); + } + + User registUser = new User(account, password, email); + InMemoryUserRepository.save(registUser); + + if (log.isInfoEnabled()) { + log.info(String.format("%s %s", "회원가입 성공!", registUser)); + } + + return ViewResolver.resolveView(LOGIN_URI); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java new file mode 100644 index 0000000000..a332c7dd9b --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpResponse.java @@ -0,0 +1,38 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; + +public class HttpResponse { + + private final String body; + private final HttpStatus httpStatus; + private final ContentType contentType; + private final Map headers = new HashMap<>(); + + public HttpResponse(String body, HttpStatus httpStatus, ContentType contentType) { + this.body = body; + this.httpStatus = httpStatus; + this.contentType = contentType; + } + + public String getBody() { + return body; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String getContentType() { + return contentType.getType(); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + + public Map getHeaders() { + return Map.copyOf(headers); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java new file mode 100644 index 0000000000..0c214d1da6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpStatus.java @@ -0,0 +1,26 @@ +package org.apache.coyote.http11; + +public enum HttpStatus { + OK(200, "OK"), + CREATED(201, "Created"), + FOUND(302, "Found"), + UNAUTHORIZED(401, "Unauthorized"), + NOT_FOUND(404, "Not Found"), + ; + + private final int code; + private final String message; + + HttpStatus(int code, String message) { + this.code = code; + this.message = message; + } + + public String getMessage() { + return message; + } + + public int getCode() { + return code; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java new file mode 100644 index 0000000000..d433249461 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/ViewResolver.java @@ -0,0 +1,45 @@ +package org.apache.coyote.http11; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +import static org.apache.coyote.http11.ContentType.TEXT_HTML; + +public class ViewResolver { + private static final String DEFAULT_FILE_ROUTE = "static"; + + private ViewResolver() { + } + + public static HttpResponse resolveView(String path) throws IOException { + String filePath = path; + if (!filePath.contains(".")) { + filePath += ".html"; + } + + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + final URL resource = systemClassLoader.getResource(String.format("%s%s", DEFAULT_FILE_ROUTE, filePath)); + + if (resource == null) { + final URL notFoundUrl = systemClassLoader.getResource(String.format("%s/%s", DEFAULT_FILE_ROUTE, "404.html")); + File notFound = new File(notFoundUrl.getPath()); + String body = new String(Files.readAllBytes(notFound.toPath())); + return new HttpResponse(body, HttpStatus.NOT_FOUND, ContentType.from(notFound.getName())); + } + + File file = new File(resource.getPath()); + String body = new String(Files.readAllBytes(file.toPath())); + + return new HttpResponse(body, HttpStatus.OK, ContentType.from(file.getName())); + } + + public static HttpResponse routePath(String path) throws IOException { + if (path.equals("/")) { + return new HttpResponse("Hello world!", HttpStatus.OK, TEXT_HTML); + } + + return ViewResolver.resolveView(path); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java new file mode 100644 index 0000000000..7b4045681f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/requests/HttpCookie.java @@ -0,0 +1,40 @@ +package org.apache.coyote.requests; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +public class HttpCookie { + private final Map items; + + public HttpCookie(Map items) { + this.items = items; + } + + public static HttpCookie from(String string) { + if (string == null || string.isBlank()) { + return new HttpCookie(new HashMap<>()); + } + return Arrays.stream(string.split(" ")) + .map(line -> line.split("=")) + .collect(collectingAndThen( + toMap(line -> line[0], line -> line[1]), + HttpCookie::new + )); + } + + public String get(String key) { + return items.get(key); + } + + public String getJSessionId(boolean create) { + if (create) { + items.put("JSESSIONID", UUID.randomUUID().toString()); + } + return get("JSESSIONID"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java new file mode 100644 index 0000000000..261368be6c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/requests/RequestBody.java @@ -0,0 +1,18 @@ +package org.apache.coyote.requests; + +public class RequestBody { + + private final String item; + + public RequestBody() { + this.item = ""; + } + + public RequestBody(String item) { + this.item = item; + } + + public String getItem() { + return item; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java new file mode 100644 index 0000000000..5cc3cc03c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/requests/RequestHeader.java @@ -0,0 +1,28 @@ +package org.apache.coyote.requests; + +import java.util.Arrays; +import java.util.Map; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toMap; + +public class RequestHeader { + private final Map items; + + private RequestHeader(Map items) { + this.items = items; + } + + public static RequestHeader from(String string) { + return Arrays.stream(string.split("\r\n")) + .map(line -> line.split(": ")) + .collect(collectingAndThen( + toMap(line -> line[0], line -> line[1]), + RequestHeader::new + )); + } + + public String get(String key) { + return items.get(key); + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..bc933357f2 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+
diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..fbebf5c3fc 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,8 @@ package nextstep.org.apache.coyote.http11; -import support.StubSocket; import org.apache.coyote.http11.Http11Processor; import org.junit.jupiter.api.Test; +import support.StubSocket; import java.io.File; import java.io.IOException; @@ -33,6 +33,33 @@ void process() { assertThat(socket.output()).isEqualTo(expected); } + @Test + void notFound() throws IOException { + // given + final String httpRequest= String.join("\r\n", + "GET /anyThing.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/404.html"); + var expected = "HTTP/1.1 404 Not Found \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 2426 \r\n" + + "\r\n"+ + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + @Test void index() throws IOException { // given @@ -59,4 +86,84 @@ void index() throws IOException { assertThat(socket.output()).isEqualTo(expected); } + + @Test + void css() throws IOException { + // given + final String httpRequest= String.join("\r\n", + "GET /css/styles.css HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/css/styles.css"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/css;charset=utf-8 \r\n" + + "Content-Length: 211991 \r\n" + + "\r\n"+ + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void login() throws IOException { + // given + final String httpRequest= String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + var expected = "HTTP/1.1 302 Found \r\n" + + "Location: /login \r\n" + + "\r\n"+ + "/login"; + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void loginWithParam() throws IOException { + // given + final String httpRequest= String.join("\r\n", + "GET /login?account=gugu&password=password HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/login.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 3797 \r\n" + + "\r\n"+ + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } }