diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..c8c39ed2bd 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -3,8 +3,10 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -18,7 +20,7 @@ class FileTest { /** * resource 디렉터리 경로 찾기 - * + *

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

* 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException { final String fileName = "nextstep.txt"; + final String fileUrl = getClass().getClassLoader().getResource(fileName).getFile(); - // todo - final Path path = null; - - // todo - final List actual = Collections.emptyList(); + final List actual = Files.readAllLines(Path.of(fileUrl)); assertThat(actual).containsOnly("nextstep"); } diff --git a/study/src/test/java/study/IOStreamTest.java b/study/src/test/java/study/IOStreamTest.java index 47a79356b6..a908f66871 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -4,19 +4,30 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -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 static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +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)을 처리할 수 있다. @@ -26,7 +37,7 @@ class IOStreamTest { /** * OutputStream 학습하기 - * + *

* 자바의 기본 출력 클래스는 java.io.OutputStream이다. * OutputStream의 write(int b) 메서드는 기반 메서드이다. * public abstract void write(int b) throws IOException; @@ -39,7 +50,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바이트 이상을 한 번에 전송 할 수 있어 훨씬 효율적이다. @@ -48,12 +59,8 @@ class OutputStream_학습_테스트 { void OutputStream은_데이터를_바이트로_처리한다() throws IOException { final byte[] bytes = {110, 101, 120, 116, 115, 116, 101, 112}; final OutputStream outputStream = new ByteArrayOutputStream(bytes.length); - - /** - * todo - * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 - */ - + + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -63,7 +70,7 @@ class OutputStream_학습_테스트 { /** * 효율적인 전송을 위해 스트림에서 버퍼링을 사용 할 수 있다. * BufferedOutputStream 필터를 연결하면 버퍼링이 가능하다. - * + *

* 버퍼링을 사용하면 OutputStream을 사용할 때 flush를 사용하자. * flush() 메서드는 버퍼가 아직 가득 차지 않은 상황에서 강제로 버퍼의 내용을 전송한다. * Stream은 동기(synchronous)로 동작하기 때문에 버퍼가 찰 때까지 기다리면 @@ -73,11 +80,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 +94,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 +104,12 @@ class OutputStream_학습_테스트 { /** * InputStream 학습하기 - * + *

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

* InputStream의 서브 클래스(subclass)는 특정 매체에 데이터를 읽기 위해 read() 메서드를 사용한다. */ @Nested @@ -124,11 +125,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 +140,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 +150,7 @@ class InputStream_학습_테스트 { /** * FilterStream 학습하기 - * + *

* 필터는 필터 스트림, reader, writer로 나뉜다. * 필터는 바이트를 다른 데이터 형식으로 변환 할 때 사용한다. * reader, writer는 UTF-8, ISO 8859-1 같은 형식으로 인코딩된 텍스트를 처리하는 데 사용된다. @@ -169,12 +164,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,16 +192,23 @@ 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 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); final StringBuilder actual = new StringBuilder(); + String line = bufferedReader.readLine(); + while (line != null) { + actual.append(line + "\r\n"); + line = bufferedReader.readLine(); + } + assertThat(actual).hasToString(emoji); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 7f1b2c7e96..d50f800ebf 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,43 @@ package org.apache.coyote.http11; +import nextstep.jwp.db.InMemoryUserRepository; import nextstep.jwp.exception.UncheckedServletException; +import nextstep.jwp.model.User; import org.apache.coyote.Processor; +import org.apache.coyote.http11.cookie.HttpCookie; +import org.apache.coyote.http11.request.HttpMethod; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestStartLine; +import org.apache.coyote.http11.response.HttpResponseEntity; +import org.apache.coyote.http11.session.HttpSession; +import org.apache.coyote.http11.session.SessionManager; 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.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; public class Http11Processor implements Runnable, Processor { - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + private static final SessionManager sessionManager = SessionManager.create(); + private static final String STATIC = "static"; + private static final String INDEX_HTML = "/index.html"; + private static final String UNAUTHORIZED_HTML = "/401.html"; + private static final String LOGIN_HTML = "/login.html"; + private static final String BODY_DELIMITER = "&"; + private static final String PAIR_DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; private final Socket connection; @@ -26,22 +53,93 @@ public void run() { @Override public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { + try (final var bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + final var bufferedWriter = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) { - final var responseBody = "Hello world!"; + final HttpRequest httpRequest = HttpRequest.from(bufferedReader); + final HttpCookie httpCookie = makeHttpCookie(httpRequest); - 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); + final HttpResponseEntity httpResponseEntity = makeResponseEntity(httpRequest, httpCookie); + final String response = httpResponseEntity.makeResponse(); - outputStream.write(response.getBytes()); - outputStream.flush(); + bufferedWriter.write(response); + bufferedWriter.flush(); } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } } + + private HttpCookie makeHttpCookie(final HttpRequest httpRequest) { + if (httpRequest.hasCookie()) { + return HttpCookie.from(httpRequest.getCookie()); + } + return HttpCookie.empty(); + } + + private HttpResponseEntity makeResponseEntity(final HttpRequest httpRequest, final HttpCookie cookie) throws IOException { + final HttpRequestStartLine startLine = httpRequest.getStartLine(); + final String path = startLine.getPath(); + + if (startLine.getHttpMethod().equals(HttpMethod.POST)) { + if (path.startsWith("/login")) { + return login(httpRequest); + } + if (path.startsWith("/register")) { + return register(httpRequest); + } + } + if (path.startsWith("/login") && cookie.hasJSESSIONID()) { + final String jsessionid = cookie.getJSESSIONID(); + HttpSession httpSession = sessionManager.findSession(jsessionid); + if (Objects.isNull(httpSession)) { + return HttpResponseEntity.ok(LOGIN_HTML, makeResponseBody(LOGIN_HTML)); + } + return HttpResponseEntity.found(INDEX_HTML); + } + return HttpResponseEntity.ok(path, makeResponseBody(path)); + } + + private HttpResponseEntity login(final HttpRequest httpRequest) throws IOException { + final Map loginData = parseFormData(httpRequest.getBody()); + final User user = InMemoryUserRepository.findByAccount(loginData.get("account")) + .orElseThrow(); + if (user.checkPassword(loginData.get("password"))) { + final HttpCookie newCookie = HttpCookie.create(); + saveSession(newCookie, user); + return HttpResponseEntity.found(INDEX_HTML) + .setCookie(newCookie.getJSESSIONID()); + } + return HttpResponseEntity.ok(UNAUTHORIZED_HTML, makeResponseBody(UNAUTHORIZED_HTML)); + } + + private void saveSession(final HttpCookie newCookie, final User user) { + final HttpSession httpSession = new HttpSession(newCookie.getJSESSIONID()); + httpSession.setAttribute("user", user); + sessionManager.add(httpSession); + } + + private HttpResponseEntity register(final HttpRequest httpRequest) throws IOException { + final Map registerData = parseFormData(httpRequest.getBody()); + InMemoryUserRepository.save(new User(registerData.get("account"), registerData.get("password"), registerData.get("email"))); + return HttpResponseEntity.ok(INDEX_HTML, makeResponseBody(INDEX_HTML)); + } + + private Map parseFormData(final String body) { + return Arrays.stream(body.split(BODY_DELIMITER)) + .map(data -> data.split(PAIR_DELIMITER)) + .collect(Collectors.toMap( + data -> data[KEY_INDEX], + data -> data[VALUE_INDEX]) + ); + } + + private String makeResponseBody(final String path) throws IOException { + if (path.equals("/")) { + return "Hello world!"; + } + final ClassLoader classLoader = getClass().getClassLoader(); + final String filePath = classLoader.getResource(STATIC + path).getPath(); + final String fileContent = new String(Files.readAllBytes(Path.of(filePath))); + return String.join("\r\n", fileContent); + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java new file mode 100644 index 0000000000..d286c2ea6a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/cookie/HttpCookie.java @@ -0,0 +1,47 @@ +package org.apache.coyote.http11.cookie; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class HttpCookie { + private static final String JSESSIONID = "JSESSIONID"; + private static final String LIST_DELIMITER = "; "; + private static final String PAIR_DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map cookie; + + private HttpCookie(final Map cookie) { + this.cookie = cookie; + } + + public static HttpCookie from(final String cookie) { + return new HttpCookie(Arrays.stream(cookie.split(LIST_DELIMITER)) + .map(data -> data.split(PAIR_DELIMITER)) + .collect(Collectors.toMap( + data -> data[KEY_INDEX], + data -> data[VALUE_INDEX]) + ) + ); + } + + public static HttpCookie empty() { + return new HttpCookie(Collections.emptyMap()); + } + + public static HttpCookie create() { + return new HttpCookie(Map.of(JSESSIONID, String.valueOf(UUID.randomUUID()))); + } + + public boolean hasJSESSIONID() { + return this.cookie.containsKey(JSESSIONID); + } + + public String getJSESSIONID() { + return this.cookie.get(JSESSIONID); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java new file mode 100644 index 0000000000..024086f1f4 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpMethod.java @@ -0,0 +1,6 @@ +package org.apache.coyote.http11.request; + +public enum HttpMethod { + GET, + POST +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java new file mode 100644 index 0000000000..83c51359ca --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,63 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class HttpRequest { + private final HttpRequestStartLine startLine; + private final HttpRequestHeaders headers; + private final HttpRequestBody body; + + private HttpRequest(final HttpRequestStartLine startLine, final HttpRequestHeaders headers, final HttpRequestBody body) { + this.startLine = startLine; + this.headers = headers; + this.body = body; + } + + public static HttpRequest from(final BufferedReader bufferedReader) throws IOException { + final List requestHeader = extractRequestHeader(bufferedReader); + final HttpRequestStartLine startLine = HttpRequestStartLine.from(requestHeader.get(0)); + final HttpRequestHeaders headers = HttpRequestHeaders.from(requestHeader.subList(1, requestHeader.size())); + final HttpRequestBody requestBody = extractRequestBody(bufferedReader, headers); + return new HttpRequest(startLine, headers, requestBody); + } + + private static List extractRequestHeader(final BufferedReader bufferedReader) throws IOException { + final List requestHeader = new ArrayList<>(); + String line = bufferedReader.readLine(); + while (!"".equals(line) && line != null) { + requestHeader.add(line); + line = bufferedReader.readLine(); + } + return requestHeader; + } + + private static HttpRequestBody extractRequestBody(final BufferedReader bufferedReader, final HttpRequestHeaders requestHeader) throws IOException { + final String contentLength = requestHeader.get("Content-Length"); + if (contentLength == null) { + return HttpRequestBody.empty(); + } + final int length = Integer.parseInt(contentLength); + final char[] buffer = new char[length]; + bufferedReader.read(buffer, 0, length); + return HttpRequestBody.from(new String(buffer)); + } + + public boolean hasCookie() { + return headers.hasCookie(); + } + + public HttpRequestStartLine getStartLine() { + return startLine; + } + + public String getBody() { + return body.getBody(); + } + + public String getCookie() { + return headers.get("Cookie"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java new file mode 100644 index 0000000000..f1e84ee1f9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestBody.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.request; + +import org.apache.commons.lang3.StringUtils; + +public class HttpRequestBody { + private final String body; + + private HttpRequestBody(final String body) { + this.body = body; + } + + public static HttpRequestBody from(final String httpRequestBody) { + return new HttpRequestBody(httpRequestBody); + } + + public static HttpRequestBody empty() { + return new HttpRequestBody(StringUtils.EMPTY); + } + + public String getBody() { + return body; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java new file mode 100644 index 0000000000..6ade8d60ab --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestHeaders.java @@ -0,0 +1,35 @@ +package org.apache.coyote.http11.request; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class HttpRequestHeaders { + private static final String PAIR_DELIMITER = ": "; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map headers; + + private HttpRequestHeaders(final Map headers) { + this.headers = headers; + } + + public static HttpRequestHeaders from(final List httpRequestHeader) { + return new HttpRequestHeaders(httpRequestHeader.stream() + .map(data -> data.split(PAIR_DELIMITER)) + .collect(Collectors.toMap( + data -> data[KEY_INDEX], + data -> data[VALUE_INDEX]) + ) + ); + } + + public boolean hasCookie() { + return headers.containsKey("Cookie"); + } + + public String get(final String key) { + return headers.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java new file mode 100644 index 0000000000..7550c3cecc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestStartLine.java @@ -0,0 +1,49 @@ +package org.apache.coyote.http11.request; + +public class HttpRequestStartLine { + private static final String QUERY_DELIMITER = "?"; + private static final int METHOD_INDEX = 0; + private static final int PATH_INDEX = 1; + private static final int VERSION_INDEX = 2; + + private final HttpMethod httpMethod; + private final String path; + private final Query query; + private final String version; + + private HttpRequestStartLine(final HttpMethod httpMethod, final String path, final Query query, final String version) { + this.httpMethod = httpMethod; + this.path = path; + this.query = query; + this.version = version; + } + + public static HttpRequestStartLine from(final String startLine) { + final String[] startLines = startLine.split(" "); + String path = startLines[PATH_INDEX]; + final Query query = extractQuery(path); + if (path.equals("/login") || path.equals("/register")) { + path = path + ".html"; + } + return new HttpRequestStartLine(HttpMethod.valueOf(startLines[METHOD_INDEX]), path, query, startLines[VERSION_INDEX]); + } + + private static Query extractQuery(final String path) { + if (isExistQuery(path)) { + return Query.from(path.substring(path.indexOf(QUERY_DELIMITER) + PATH_INDEX)); + } + return Query.empty(); + } + + private static boolean isExistQuery(final String path) { + return path.contains(QUERY_DELIMITER); + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public String getPath() { + return path; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java new file mode 100644 index 0000000000..9e7cd03a1d --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Query.java @@ -0,0 +1,32 @@ +package org.apache.coyote.http11.request; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +public class Query { + private static final String QUERY_DELIMITER = "&"; + private static final String PAIR_DELIMITER = "="; + private static final int KEY_INDEX = 0; + private static final int VALUE_INDEX = 1; + + private final Map query; + + private Query(final Map query) { + this.query = query; + } + + public static Query from(final String query) { + return new Query(Arrays.stream(query.split(QUERY_DELIMITER)) + .map(data -> data.split(PAIR_DELIMITER)) + .collect(Collectors.toMap( + data -> data[KEY_INDEX], + data -> data[VALUE_INDEX] + ))); + } + + public static Query empty() { + return new Query(Collections.emptyMap()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java new file mode 100644 index 0000000000..6116aa8635 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponseEntity.java @@ -0,0 +1,54 @@ +package org.apache.coyote.http11.response; + +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.StringJoiner; + +public class HttpResponseEntity { + private final Map header; + private final String body; + + private HttpResponseEntity(final Map header, final String body) { + this.header = header; + this.body = body; + } + + public static HttpResponseEntity ok(final String path, final String body) { + final Map requestHeader = new LinkedHashMap<>(); + requestHeader.put("HTTP/1.1 ", HttpStatusCode.OK.message()); + requestHeader.put("Content-Type: ", makeContentType(path) + ";charset=utf-8 "); + requestHeader.put("Content-Length: ", body.getBytes().length + " "); + return new HttpResponseEntity(requestHeader, body); + } + + private static String makeContentType(final String path) { + if (path.endsWith("css")) { + return "text/css"; + } + return "text/html"; + } + + public static HttpResponseEntity found(final String path) { + final Map requestHeader = new LinkedHashMap<>(); + requestHeader.put("HTTP/1.1 ", HttpStatusCode.FOUND.message()); + requestHeader.put("Location: ", path); + return new HttpResponseEntity(requestHeader, StringUtils.EMPTY); + } + + public HttpResponseEntity setCookie(final String jsessionid) { + header.put("Set-Cookie: JSESSIONID=", jsessionid); + return this; + } + + public String makeResponse() { + StringJoiner stringJoiner = new StringJoiner("\r\n"); + for (final Map.Entry entry : header.entrySet()) { + stringJoiner.add(entry.getKey() + entry.getValue()); + } + stringJoiner.add(""); + stringJoiner.add(body); + return stringJoiner.toString(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java new file mode 100644 index 0000000000..483d4db414 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpStatusCode.java @@ -0,0 +1,19 @@ +package org.apache.coyote.http11.response; + +public enum HttpStatusCode { + OK(200, "OK"), + FOUND(302, "Found"), + ; + + private final int code; + private final String text; + + HttpStatusCode(int code, String text) { + this.code = code; + this.text = text; + } + + public String message() { + return String.join(" ", String.valueOf(code), text, ""); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java b/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java new file mode 100644 index 0000000000..f2c7147484 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/HttpSession.java @@ -0,0 +1,30 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; + +public class HttpSession { + + private final String id; + private final Map items = new HashMap<>(); + + public HttpSession(final String id) { + this.id = id; + } + + public Object getAttribute(final String name) { + return items.get(name); + } + + public void setAttribute(final String name, final Object value) { + items.put(name, value); + } + + public void removeAttribute(final String name) { + items.remove(name); + } + + public String getId() { + return id; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java new file mode 100644 index 0000000000..49a42e7dc6 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/session/SessionManager.java @@ -0,0 +1,23 @@ +package org.apache.coyote.http11.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionManager { + private static final Map SESSIONS = new HashMap<>(); + + private SessionManager() { + } + + public static SessionManager create() { + return new SessionManager(); + } + + public void add(final HttpSession httpSession) { + SESSIONS.put(httpSession.getId(), httpSession); + } + + public HttpSession findSession(final String id) { + return SESSIONS.get(String.valueOf(id)); + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..ff5bdacd5d 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,70 @@ - - - - - - - 로그인 - - - - -

-
-
-
-
-
-
-

로그인

-
-
-
- - -
-
- - -
-
- -
-
+ + + + + + + 로그인 + + + + +
+
+
+
+
+
+
+

로그인

+
+
+
+ +
- +
+ +
+
+
+
-
+
-
+
+
- - - - + + + + + + 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..f25837ebbb 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; @@ -36,7 +36,7 @@ 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 ", @@ -54,7 +54,149 @@ void index() throws IOException { 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"+ + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + 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 ", + "Accept: text/css,*/*;q=0.1", + "", + ""); + + 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", + "GET /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 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 3447 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void loginFail() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 26", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*", + "", + "account=gugu&password=1234"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/401.html"); + var expected = "HTTP/1.1 200 OK \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 register() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /register 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/register.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 4319 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } + + @Test + void registerSuccess() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "POST /register HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "Content-Length: 80", + "Content-Type: application/x-www-form-urlencoded", + "Accept: */*", + "", + "account=gugu&password=password&email=hkkang%40woowahan.com"); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 5564 \r\n" + + "\r\n" + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); assertThat(socket.output()).isEqualTo(expected);