diff --git a/study/build.gradle b/study/build.gradle index 5c69542f84..87a1f0313c 100644 --- a/study/build.gradle +++ b/study/build.gradle @@ -19,7 +19,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.4.1' diff --git a/study/src/main/java/cache/com/example/GreetingController.java b/study/src/main/java/cache/com/example/GreetingController.java index c0053cda42..2416d9ac6e 100644 --- a/study/src/main/java/cache/com/example/GreetingController.java +++ b/study/src/main/java/cache/com/example/GreetingController.java @@ -1,12 +1,11 @@ package cache.com.example; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletResponse; - @Controller public class GreetingController { diff --git a/study/src/main/java/cache/com/example/version/ResourceVersion.java b/study/src/main/java/cache/com/example/version/ResourceVersion.java index 27a2f22813..215ba7a705 100644 --- a/study/src/main/java/cache/com/example/version/ResourceVersion.java +++ b/study/src/main/java/cache/com/example/version/ResourceVersion.java @@ -1,10 +1,9 @@ package cache.com.example.version; -import org.springframework.stereotype.Component; - import jakarta.annotation.PostConstruct; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; @Component public class ResourceVersion { diff --git a/study/src/main/resources/application.yml b/study/src/main/resources/application.yml index 4e8655a962..e3503a5fb9 100644 --- a/study/src/main/resources/application.yml +++ b/study/src/main/resources/application.yml @@ -6,4 +6,5 @@ server: accept-count: 1 max-connections: 1 threads: + min-spare: 2 max: 2 diff --git a/study/src/test/java/study/FileTest.java b/study/src/test/java/study/FileTest.java index e1b6cca042..8970522027 100644 --- a/study/src/test/java/study/FileTest.java +++ b/study/src/test/java/study/FileTest.java @@ -1,53 +1,54 @@ package study; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +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; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; /** - * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. - * File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. + * 웹서버는 사용자가 요청한 html 파일을 제공 할 수 있어야 한다. File 클래스를 사용해서 파일을 읽어오고, 사용자에게 전달한다. */ @DisplayName("File 클래스 학습 테스트") class FileTest { /** * resource 디렉터리 경로 찾기 - * - * File 객체를 생성하려면 파일의 경로를 알아야 한다. - * 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. - * resource 디렉터리의 경로는 어떻게 알아낼 수 있을까? + *

+ * File 객체를 생성하려면 파일의 경로를 알아야 한다. 자바 애플리케이션은 resource 디렉터리에 HTML, CSS 같은 정적 파일을 저장한다. resource 디렉터리의 경로는 어떻게 알아낼 수 + * 있을까? */ @Test void resource_디렉터리에_있는_파일의_경로를_찾는다() { final String fileName = "nextstep.txt"; // todo - final String actual = ""; + URL resource = getClass().getClassLoader().getResource(fileName); + final String actual = resource.getPath(); assertThat(actual).endsWith(fileName); } /** * 파일 내용 읽기 - * - * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. - * File, Files 클래스를 사용하여 파일의 내용을 읽어보자. + *

+ * 읽어온 파일의 내용을 I/O Stream을 사용해서 사용자에게 전달 해야 한다. File, Files 클래스를 사용하여 파일의 내용을 읽어보자. */ @Test - void 파일의_내용을_읽는다() { + void 파일의_내용을_읽는다() throws IOException, URISyntaxException { final String fileName = "nextstep.txt"; // todo - final Path path = null; + URL resource = getClass().getClassLoader().getResource(fileName); + final Path path = Path.of(resource.toURI()); // 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..f874eb22c3 100644 --- a/study/src/test/java/study/IOStreamTest.java +++ b/study/src/test/java/study/IOStreamTest.java @@ -1,5 +1,6 @@ package study; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -54,6 +55,7 @@ class OutputStream_학습_테스트 { * OutputStream 객체의 write 메서드를 사용해서 테스트를 통과시킨다 */ + outputStream.write(bytes); final String actual = outputStream.toString(); assertThat(actual).isEqualTo("nextstep"); @@ -78,7 +80,7 @@ class OutputStream_학습_테스트 { * flush를 사용해서 테스트를 통과시킨다. * ByteArrayOutputStream과 어떤 차이가 있을까? */ - + outputStream.flush(); verify(outputStream, atLeastOnce()).flush(); outputStream.close(); } @@ -96,6 +98,9 @@ class OutputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try(outputStream) { + outputStream.flush(); + } verify(outputStream, atLeastOnce()).close(); } @@ -128,7 +133,7 @@ class InputStream_학습_테스트 { * todo * inputStream에서 바이트로 반환한 값을 문자열로 어떻게 바꿀까? */ - final String actual = ""; + final String actual = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); assertThat(actual).isEqualTo("🤩"); assertThat(inputStream.read()).isEqualTo(-1); @@ -148,6 +153,9 @@ class InputStream_학습_테스트 { * try-with-resources를 사용한다. * java 9 이상에서는 변수를 try-with-resources로 처리할 수 있다. */ + try(inputStream) { + + } verify(inputStream, atLeastOnce()).close(); } @@ -169,12 +177,13 @@ 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(); + bufferedInputStream.close(); assertThat(bufferedInputStream).isInstanceOf(FilterInputStream.class); assertThat(actual).isEqualTo("필터에 연결해보자.".getBytes()); @@ -204,9 +213,22 @@ class InputStreamReader_학습_테스트 { "😋😛😝😜🤪🤨🧐🤓😎🥸🤩", ""); final InputStream inputStream = new ByteArrayInputStream(emoji.getBytes()); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream); final StringBuilder actual = new StringBuilder(); + try(BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while (true) { + String read = bufferedReader.readLine(); + if (read == null) { + break; + } + actual.append(read).append("\r\n"); + } + } catch (IOException e) { + e.printStackTrace(); + } + assertThat(actual).hasToString(emoji); } } diff --git a/tomcat/src/main/java/com/techcourse/controller/AbstractController.java b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java new file mode 100644 index 0000000000..9601027862 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/AbstractController.java @@ -0,0 +1,26 @@ +package com.techcourse.controller; + +import java.io.IOException; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public abstract class AbstractController implements Controller { + + @Override + public void service(HttpRequest request, HttpResponse response) throws IOException { + String method = request.getRequestLine().getMethod(); + if (method.equals("POST")) { + doPost(request, response); + } else if (method.equals("GET")) { + doGet(request, response); + } else { + response.setStatus405(); + } + } + + protected void doPost(HttpRequest request, HttpResponse response) throws IOException { + } + + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/Controller.java b/tomcat/src/main/java/com/techcourse/controller/Controller.java new file mode 100644 index 0000000000..3e0cb98990 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/Controller.java @@ -0,0 +1,9 @@ +package com.techcourse.controller; + +import java.io.IOException; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public interface Controller { + void service(HttpRequest request, HttpResponse response) throws IOException; +} diff --git a/tomcat/src/main/java/com/techcourse/controller/LoginController.java b/tomcat/src/main/java/com/techcourse/controller/LoginController.java new file mode 100644 index 0000000000..184a98099e --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/LoginController.java @@ -0,0 +1,96 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import com.techcourse.session.Session; +import com.techcourse.session.SessionManager; +import com.techcourse.view.View; +import com.techcourse.view.ViewResolver; +import org.apache.coyote.http11.HttpCookie; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public class LoginController extends AbstractController { + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + Session session = extractSession(request); + if (session != null) { + User user = (User) session.getAttribute("user"); + if (user != null) { + responseLoginSuccess(response, session); + return; + } + } + responseLoginPage(request, response); + } + + @Override + protected void doPost(HttpRequest request, HttpResponse response) { + if (!request.hasBodyData()) { + throw new IllegalArgumentException("Query string is missing in the request"); + } + + Map requestFormData = request.getFormData(); + String userName = requestFormData.get("account"); + String password = requestFormData.get("password"); + + Optional account = InMemoryUserRepository.findByAccount(userName); + if (account.isEmpty()) { + responseLoginFail(response); + } else { + User user = account.get(); + if (user.checkPassword(password)) { + Session session = saveSession(user); + responseLoginSuccess(response, session); + } else { + responseLoginFail(response); + } + } + } + + private Session extractSession(HttpRequest request) { + String cookie = request.getCookie(); + if (cookie == null) { + Map cookies = new HashMap<>(); + for (String cookieParts : cookie.split(" ")) { + String[] keyAndValue = cookieParts.split("="); + cookies.put(keyAndValue[0], keyAndValue[1]); + } + + String jsessionId = cookies.get("JSESSIONID"); + return SessionManager.findSession(jsessionId); + } + + return null; + } + + private Session saveSession(User user) { + Session session = new Session(); + session.setAttribute("user", user); + SessionManager.add(session); + return session; + } + + private void responseLoginPage(HttpRequest request, HttpResponse response) throws IOException { + View view = ViewResolver.getView("/login.html"); + response.setStatus200(); + response.setResponseBody(view.getContent()); + response.setContentType(request.getContentType()); + } + + private void responseLoginSuccess(HttpResponse response, Session session) { + response.setStatus302(); + response.setLocation("/index.html"); + response.setCookie(HttpCookie.ofJSessionId(session.getId())); + } + + private void responseLoginFail(HttpResponse response) { + response.setStatus302(); + response.setLocation("/401.html"); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/RegisterController.java b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java new file mode 100644 index 0000000000..ad68c4e7ee --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/RegisterController.java @@ -0,0 +1,58 @@ +package com.techcourse.controller; + +import com.techcourse.db.InMemoryUserRepository; +import com.techcourse.model.User; +import java.io.IOException; +import java.util.Map; +import com.techcourse.view.View; +import com.techcourse.view.ViewResolver; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RegisterController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(RegisterController.class); + + @Override + protected void doPost(HttpRequest request, HttpResponse response) { + try { + if (!request.hasBodyData()) { + throw new IllegalArgumentException("RequestBody is missing in the request"); + } + + Map requestFormData = request.getFormData(); + String account = requestFormData.get("account"); + String password = requestFormData.get("password"); + String email = requestFormData.get("email"); + + User user = new User(account, password, email); + InMemoryUserRepository.save(user); + responseRegisterSuccess(response); + + } catch (IllegalArgumentException e) { + response.setStatus400(); + response.setResponseBody(e.getMessage()); + log.info("Bad Request: {}", e.getMessage()); + + } + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + responseRegisterPage(request, response); + } + + private void responseRegisterSuccess(HttpResponse response) { + response.setStatus302(); + response.setLocation("/index.html"); + } + + private void responseRegisterPage(HttpRequest request, HttpResponse response) throws IOException { + View view = ViewResolver.getView("/register.html"); + response.setStatus200(); + response.setResponseBody(view.getContent()); + response.setContentType(request.getContentType()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/controller/ViewController.java b/tomcat/src/main/java/com/techcourse/controller/ViewController.java new file mode 100644 index 0000000000..20159bf8e7 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/controller/ViewController.java @@ -0,0 +1,50 @@ +package com.techcourse.controller; + +import java.io.IOException; +import com.techcourse.view.View; +import com.techcourse.view.ViewResolver; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.response.HttpResponse; + +public class ViewController extends AbstractController { + + @Override + protected void doPost(HttpRequest request, HttpResponse response) throws IOException { + responseNotFoundPage(request, response); + } + + @Override + protected void doGet(HttpRequest request, HttpResponse response) throws IOException { + String path = request.getRequestLine().getPath(); + + if (path.equals("/")) { + responseDefaultPage(request, response); + } else { + View view = ViewResolver.getView(path); + if (view == null) { + responseNotFoundPage(request, response); + } else { + responseResource(request, response, view); + } + } + } + + private void responseDefaultPage(HttpRequest request, HttpResponse response) { + response.setStatus200(); + response.setContentType(request.getContentType()); + response.setResponseBody("Hello world!"); + } + + private void responseNotFoundPage(HttpRequest request, HttpResponse response) throws IOException { + View notFoundView = ViewResolver.getView("/404.html"); + response.setStatus404(); + response.setResponseBody(notFoundView.getContent()); + response.setContentType(request.getContentType()); + } + + private void responseResource(HttpRequest request, HttpResponse response, View view) { + response.setStatus200(); + response.setResponseBody(view.getContent()); + response.setContentType(request.getContentType()); + } +} diff --git a/tomcat/src/main/java/com/techcourse/handler/RequestMapping.java b/tomcat/src/main/java/com/techcourse/handler/RequestMapping.java new file mode 100644 index 0000000000..d310fe3621 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/handler/RequestMapping.java @@ -0,0 +1,36 @@ +package com.techcourse.handler; + +import java.util.HashMap; +import java.util.Map; +import com.techcourse.controller.Controller; +import com.techcourse.controller.LoginController; +import com.techcourse.controller.RegisterController; +import com.techcourse.controller.ViewController; +import org.apache.coyote.http11.request.HttpRequest; + +public class RequestMapping { + + private static final RequestMapping INSTANCE = new RequestMapping(); + + private final Map controllerMapping = new HashMap<>(); + + private RequestMapping() { + controllerMapping.put("/login", new LoginController()); + controllerMapping.put("/register", new RegisterController()); + } + + public static RequestMapping getInstance() { + return INSTANCE; + } + + public Controller getController(HttpRequest request) { + String path = request.getRequestLine().getPath(); + Controller controller = controllerMapping.get(path); + if (controller == null) { + return new ViewController(); + } + + return controller; + } +} + diff --git a/tomcat/src/main/java/com/techcourse/session/Session.java b/tomcat/src/main/java/com/techcourse/session/Session.java new file mode 100644 index 0000000000..d136ac127d --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/session/Session.java @@ -0,0 +1,28 @@ +package com.techcourse.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + + private final String id; + private final Map values = new HashMap<>(); + + public Session() { + UUID uuid = UUID.randomUUID(); + this.id = uuid.toString(); + } + + public String getId() { + return id; + } + + public Object getAttribute(String name) { + return values.get(name); + } + + public void setAttribute(String name, Object value) { + values.put(name, value); + } +} diff --git a/tomcat/src/main/java/com/techcourse/session/SessionManager.java b/tomcat/src/main/java/com/techcourse/session/SessionManager.java new file mode 100644 index 0000000000..491319a4a2 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/session/SessionManager.java @@ -0,0 +1,20 @@ +package com.techcourse.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(Session session) { + SESSIONS.put(session.getId(), session); + } + + public static Session findSession(String sessionId) { + return SESSIONS.get(sessionId); + } +} diff --git a/tomcat/src/main/java/com/techcourse/view/View.java b/tomcat/src/main/java/com/techcourse/view/View.java new file mode 100644 index 0000000000..d847989864 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/view/View.java @@ -0,0 +1,14 @@ +package com.techcourse.view; + +public class View { + + private final String content; + + public View(String content) { + this.content = content; + } + + public String getContent() { + return content; + } +} diff --git a/tomcat/src/main/java/com/techcourse/view/ViewResolver.java b/tomcat/src/main/java/com/techcourse/view/ViewResolver.java new file mode 100644 index 0000000000..3e45b52759 --- /dev/null +++ b/tomcat/src/main/java/com/techcourse/view/ViewResolver.java @@ -0,0 +1,20 @@ +package com.techcourse.view; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; + +public class ViewResolver { + + public static View getView(String viewName) throws IOException { + ClassLoader classLoader = ViewResolver.class.getClassLoader(); + URL resource = classLoader.getResource("static" + viewName); + if (resource == null) { + return null; + } + + File file = new File(resource.getFile()); + return new View(new String(Files.readAllBytes(file.toPath()))); + } +} 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 bb14184757..cf870ab732 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,13 +1,19 @@ package org.apache.coyote.http11; import com.techcourse.exception.UncheckedServletException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import com.techcourse.controller.Controller; +import com.techcourse.handler.RequestMapping; import org.apache.coyote.Processor; +import org.apache.coyote.http11.request.HttpRequest; +import org.apache.coyote.http11.request.HttpRequestParser; +import org.apache.coyote.http11.response.HttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; - public class Http11Processor implements Runnable, Processor { private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); @@ -27,19 +33,21 @@ public void run() { @Override public void process(final Socket connection) { try (final var inputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); final var outputStream = connection.getOutputStream()) { - final var responseBody = "Hello world!"; + HttpRequestParser httpRequestParser = new HttpRequestParser(); + HttpRequest request = httpRequestParser.parseRequest(reader); + + HttpResponse response = new HttpResponse(); - 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); + RequestMapping requestMapping = RequestMapping.getInstance(); + Controller controller = requestMapping.getController(request); + controller.service(request, response); - outputStream.write(response.getBytes()); + outputStream.write(response.getResponse().getBytes()); outputStream.flush(); + } catch (IOException | UncheckedServletException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java new file mode 100644 index 0000000000..15aafb04cf --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/HttpCookie.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11; + +import java.util.HashMap; +import java.util.Map; + +public class HttpCookie { + + private static final String JSESSIONID = "JSESSIONID"; + + private Map cookies = new HashMap<>(); + + public static HttpCookie ofJSessionId(String sessionId) { + HttpCookie cookie = new HttpCookie(); + cookie.cookies.put(JSESSIONID, sessionId); + return cookie; + } + + private HttpCookie() { + } + + public String getJsessionId() { + return cookies.get(JSESSIONID); + } +} 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..cd5d8828d5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequest.java @@ -0,0 +1,40 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; + +public class HttpRequest { + + private final RequestLine requestLine; + private final RequestHeader header; + private final RequestBody requestBody; + + public HttpRequest(RequestLine requestLine, RequestHeader header, RequestBody requestBody) { + this.requestLine = requestLine; + this.header = header; + this.requestBody = requestBody; + } + + public String getHeader(String key) { + return header.getHeader(key); + } + + public String getContentType() { + return header.getContentType(); + } + + public String getCookie() { + return header.getCookie(); + } + + public Map getFormData() { + return requestBody.getFormData(); + } + + public boolean hasBodyData() { + return requestBody != null; + } + + public RequestLine getRequestLine() { + return requestLine; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java new file mode 100644 index 0000000000..08e8feefe9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpRequestParser.java @@ -0,0 +1,78 @@ +package org.apache.coyote.http11.request; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpRequestParser { + + private static final Logger log = LoggerFactory.getLogger(HttpRequestParser.class); + + public HttpRequest parseRequest(BufferedReader reader) throws IOException { + RequestLine requestLine = extractRequestLine(reader); + RequestHeader requestHeader = extractHeaders(reader); + RequestBody requestBody = extractRequestBody(reader, requestHeader); + + return new HttpRequest(requestLine, requestHeader, requestBody); + } + + private RequestLine extractRequestLine(BufferedReader reader) throws IOException { + String method = ""; + String path = ""; + String queryString = ""; + String protocol = ""; + + String firstLine = reader.readLine(); + + if (firstLine != null) { + log.info("[Request] requestLine: {}", firstLine); + + String[] requestLineParts = firstLine.split(" "); + method = requestLineParts[0]; + String uri = requestLineParts[1]; + protocol = requestLineParts[2]; + + int index = uri.indexOf("?"); + if (index != -1) { + path = uri.substring(0, index); + queryString = uri.substring(index + 1); + } else { + path = uri; + queryString = null; + } + } + + return new RequestLine(method, path, queryString, protocol); + } + + private RequestHeader extractHeaders(BufferedReader reader) throws IOException { + Map headers = new HashMap<>(); + while (true) { + String readLine = reader.readLine(); + if (readLine == null || readLine.isEmpty()) { + break; + } + + String[] headerLineParts = readLine.split(": "); + headers.put(headerLineParts[0], headerLineParts[1]); + } + + log.info("[Request] header: {}", headers); + return new RequestHeader(headers); + } + + private RequestBody extractRequestBody(BufferedReader reader, RequestHeader header) throws IOException { + try { + int contentLength = Integer.parseInt(header.getHeader("Content-Length")); + char[] buffer = new char[contentLength]; + reader.read(buffer, 0, contentLength); + return new RequestBody(new String(buffer)); + + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java new file mode 100644 index 0000000000..0a6e18a3ae --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestBody.java @@ -0,0 +1,28 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class RequestBody { + + private final String content; + + public RequestBody(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public Map getFormData() { + Map formData = new HashMap<>(); + String[] contentParts = content.split("&"); + for (String keyValuePair : contentParts) { + String[] keyAndValue = keyValuePair.split("="); + formData.put(keyAndValue[0], keyAndValue[1]); + } + + return formData; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java new file mode 100644 index 0000000000..5ba5c5deab --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestHeader.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.request; + +import java.util.Map; + +public class RequestHeader { + + private final Map headers; + + public RequestHeader(Map headers) { + this.headers = headers; + } + + public String getHeader(String key) { + return headers.get(key); + } + + public String getContentType() { + return headers.get("Accept").split(",")[0]; + } + + public String getCookie() { + return headers.get("Cookie"); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java new file mode 100644 index 0000000000..0287df6478 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.request; + +public class RequestLine { + + private final String method; + private final String path; + private final String queryString; + private final String version; + + public RequestLine(String method, String path, String queryString, String version) { + this.method = method; + this.path = path; + this.queryString = queryString; + this.version = version; + } + + public boolean hasQueryString() { + return queryString != null; + } + + public String getMethod() { + return method; + } + + public String getPath() { + return path; + } + + public String getQueryString() { + return queryString; + } + + public String getVersion() { + return version; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java new file mode 100644 index 0000000000..380e0e802c --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/HttpResponse.java @@ -0,0 +1,86 @@ +package org.apache.coyote.http11.response; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.coyote.http11.HttpCookie; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpResponse { + + private static final Logger log = LoggerFactory.getLogger(HttpResponse.class); + + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String LOCATION = "Location"; + private static final String CONTENT_TYPE = "Content-Type"; + private static final String CONTENT_LENGTH = "Content-Length"; + private static final String COOKIE = "Set-Cookie"; + + private StatusLine statusLine; + private Map headers = new LinkedHashMap<>(); + private String responseBody; + + public void setStatus200() { + statusLine = new StatusLine("HTTP/1.1", "200", "OK"); + } + + public void setStatus302() { + statusLine = new StatusLine("HTTP/1.1", "302", "Found"); + } + + public void setStatus400() { + statusLine = new StatusLine("HTTP/1.1", "400", "Bad Request"); + } + + public void setStatus404() { + statusLine = new StatusLine("HTTP/1.1", "404", "Not Found"); + } + + public void setStatus405() { + statusLine = new StatusLine("HTTP/1.1", "405", "Method Not Allowed"); + } + + public void setLocation(String locationPath) { + headers.put(LOCATION, locationPath); + } + + public void setContentType(String contentType) { + headers.put(CONTENT_TYPE, contentType); + } + + public void setResponseBody(String content) { + responseBody = content; + headers.put(CONTENT_LENGTH, String.valueOf(responseBody.getBytes().length)); + } + + public void setCookie(HttpCookie cookie) { + headers.put(COOKIE, "JSESSIONID=" + cookie.getJsessionId()); + } + + public String getResponse() { + StringBuilder header = new StringBuilder(); + if (headers.containsKey(LOCATION)) { + header.append(LOCATION + ": ").append(headers.get(LOCATION) + " ").append(LINE_SEPARATOR); + } + if (headers.containsKey(CONTENT_TYPE)) { + header.append(CONTENT_TYPE + ": ").append(headers.get(CONTENT_TYPE) + " ").append(LINE_SEPARATOR); + } + if (headers.containsKey(CONTENT_LENGTH)) { + header.append(CONTENT_LENGTH + ": ").append(headers.get(CONTENT_LENGTH) + " ").append(LINE_SEPARATOR); + } + if (headers.containsKey(COOKIE)) { + header.append(COOKIE + ": ").append(headers.get(COOKIE) + " ").append(LINE_SEPARATOR); + } + + String response = String.join(LINE_SEPARATOR, + statusLine.getStatusLine(), + header.toString(), + responseBody + ); + + log.info("[Response] Status Line: {}", statusLine.getStatusLine()); + log.info("[Response] Headers: {}", header); + + return response; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java new file mode 100644 index 0000000000..6c1d3b4389 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java @@ -0,0 +1,18 @@ +package org.apache.coyote.http11.response; + +public class StatusLine { + + private final String version; + private final String statusCode; + private final String statusMessage; + + public StatusLine(String version, String statusCode, String statusMessage) { + this.version = version; + this.statusCode = statusCode; + this.statusMessage = statusMessage; + } + + public String getStatusLine() { + return String.join(" ", version, statusCode, statusMessage) + " "; + } +} diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..4db95d061f 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -20,7 +20,7 @@

로그인

-
+