diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index 3dd7593507..c3b77e655f 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,11 +1,20 @@ package nextstep; +import nextstep.jwp.controller.HomeController; +import nextstep.jwp.controller.LoginController; +import nextstep.jwp.controller.RegisterController; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.controller.ControllerMapper; public class Application { public static void main(String[] args) { final var tomcat = new Tomcat(); + + ControllerMapper.register("/", new HomeController()); + ControllerMapper.register("/login", new LoginController()); + ControllerMapper.register("/register", new RegisterController()); + tomcat.start(); } } diff --git a/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java new file mode 100644 index 0000000000..e87dcabcd7 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/HomeController.java @@ -0,0 +1,20 @@ +package nextstep.jwp.controller; + +import org.apache.coyote.controller.AbstractController; +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.response.Response; + +import static org.apache.coyote.http11.response.StatusCode.METHOD_NOT_ALLOWED; + +public class HomeController extends AbstractController { + + @Override + protected void doPost(final Request request, final Response response) { + response.setStatusCode(METHOD_NOT_ALLOWED); + } + + @Override + protected void doGet(final Request request, final Response response) { + response.writeBody("Hello world!"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java new file mode 100644 index 0000000000..4b5a58319d --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/LoginController.java @@ -0,0 +1,62 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.controller.AbstractController; +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.request.Session; +import org.apache.coyote.http11.response.Response; +import org.apache.coyote.http11.response.StatusCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static org.apache.coyote.http11.response.StatusCode.UNAUTHORIZED; + +public class LoginController extends AbstractController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + + @Override + protected void doPost(final Request request, final Response response) { + final Session session = request.getSession(); + final Optional account = request.getParameter("account"); + + if (account.isEmpty()) { + response.redirect("/login.html"); + return; + } + + final Optional maybeUser = InMemoryUserRepository.findByAccount(account.get()); + if (maybeUser.isEmpty()) { + response.setStatusCode(UNAUTHORIZED); + response.writeStaticResource("/401.html"); + return; + } + final User findUser = maybeUser.get(); + final Optional password = request.getParameter("password"); + if (password.isEmpty() || !findUser.checkPassword(password.get())) { + response.setStatusCode(UNAUTHORIZED); + response.writeStaticResource("/401.html"); + return; + } + log.info("user: {}", findUser); + + session.setAttribute("user", findUser); + + response.redirect("/index.html"); + } + + @Override + protected void doGet(final Request request, final Response response) { + final Session session = request.getSession(); + final User user = (User) session.getAttribute("user"); + if (user != null) { + response.redirect("/index.html"); + return; + } + response.setStatusCode(StatusCode.CREATED); + response.writeStaticResource("/login.html"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java new file mode 100644 index 0000000000..6b5eae4af0 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/controller/RegisterController.java @@ -0,0 +1,30 @@ +package nextstep.jwp.controller; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; +import org.apache.coyote.controller.AbstractController; +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.response.Response; + +public class RegisterController extends AbstractController { + + @Override + protected void doPost(final Request request, final Response response) { + final String account = request.getParameter("account") + .orElseThrow(() -> new IllegalArgumentException("계정 입력이 잘못되었습니다.")); + final String password = request.getParameter("password") + .orElseThrow(() -> new IllegalArgumentException("비밀번호 입력이 잘못되었습니다.")); + final String email = request.getParameter("email") + .orElseThrow(() -> new IllegalArgumentException("이메일 입력이 잘못되었습니다.")); + + final User user = new User(account, password, email); + InMemoryUserRepository.save(user); + + response.redirect("/login"); + } + + @Override + protected void doGet(final Request request, final Response response) { + response.writeStaticResource("/register.html"); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..c7decebe72 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -23,5 +23,6 @@ public static Optional findByAccount(String account) { return Optional.ofNullable(database.get(account)); } - private InMemoryUserRepository() {} + private InMemoryUserRepository() { + } } diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..6b5f379283 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -36,14 +36,12 @@ public interface Manager { * specified session id (if any); otherwise return null. * * @param id The session id for the session to be returned - * - * @exception IllegalStateException if a new session cannot be - * instantiated for any reason - * @exception IOException if an input/output error occurs while - * processing this request - * * @return the request session or {@code null} if a session with the - * requested ID could not be found + * requested ID could not be found + * @throws IllegalStateException if a new session cannot be + * instantiated for any reason + * @throws IOException if an input/output error occurs while + * processing this request */ HttpSession findSession(String id) throws IOException; diff --git a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java index 3b2c4dda7c..ab2e24a4e0 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -8,6 +8,7 @@ import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.concurrent.*; public class Connector implements Runnable { @@ -15,15 +16,28 @@ public class Connector implements Runnable { private static final int DEFAULT_PORT = 8080; private static final int DEFAULT_ACCEPT_COUNT = 100; + private static final long DEFAULT_KEEP_ALIVE_ALIVE = 60; + public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS; private final ServerSocket serverSocket; private boolean stopped; + private final ExecutorService executorService; public Connector() { this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); } public Connector(final int port, final int acceptCount) { + this.executorService = Executors.newCachedThreadPool(); + this.serverSocket = createServerSocket(port, acceptCount); + this.stopped = false; + } + + public Connector(final int port, final int acceptCount, final int maxThreads) { + final int coreThreadCount = Runtime.getRuntime().availableProcessors() * 2; + final BlockingQueue workQueue = new LinkedBlockingQueue<>(DEFAULT_ACCEPT_COUNT); + + this.executorService = new ThreadPoolExecutor(coreThreadCount, maxThreads, DEFAULT_KEEP_ALIVE_ALIVE, DEFAULT_TIME_UNIT, workQueue); this.serverSocket = createServerSocket(port, acceptCount); this.stopped = false; } @@ -39,7 +53,7 @@ private ServerSocket createServerSocket(final int port, final int acceptCount) { } public void start() { - var thread = new Thread(this); + final var thread = new Thread(this); thread.setDaemon(true); thread.start(); stopped = false; @@ -66,14 +80,15 @@ private void process(final Socket connection) { if (connection == null) { return; } - var processor = new Http11Processor(connection); - new Thread(processor).start(); + final var processor = new Http11Processor(connection); + executorService.submit(processor); } public void stop() { stopped = true; try { serverSocket.close(); + executorService.shutdown(); } catch (IOException e) { log.error(e.getMessage(), e); } diff --git a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java index 205159e95b..ce9ead472d 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -11,7 +11,7 @@ public class Tomcat { private static final Logger log = LoggerFactory.getLogger(Tomcat.class); public void start() { - var connector = new Connector(); + var connector = new Connector(8080, 100, 250); connector.start(); try { diff --git a/tomcat/src/main/java/org/apache/coyote/controller/AbstractController.java b/tomcat/src/main/java/org/apache/coyote/controller/AbstractController.java new file mode 100644 index 0000000000..26f7430117 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/controller/AbstractController.java @@ -0,0 +1,31 @@ +package org.apache.coyote.controller; + +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.response.Response; + +import static org.apache.coyote.http11.request.RequestMethod.GET; +import static org.apache.coyote.http11.request.RequestMethod.POST; +import static org.apache.coyote.http11.response.StatusCode.METHOD_NOT_ALLOWED; + +public abstract class AbstractController implements Controller { + + @Override + public void service(final Request request, + final Response response) { + if (request.getRequestLine().getRequestMethod() == GET) { + doGet(request, response); + return; + } + if (request.getRequestLine().getRequestMethod() == POST) { + doPost(request, response); + return; + } + response.setStatusCode(METHOD_NOT_ALLOWED); + } + + protected abstract void doPost(final Request request, + final Response response); + + protected abstract void doGet(final Request request, + final Response response); +} diff --git a/tomcat/src/main/java/org/apache/coyote/controller/Controller.java b/tomcat/src/main/java/org/apache/coyote/controller/Controller.java new file mode 100644 index 0000000000..acf4dac106 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/controller/Controller.java @@ -0,0 +1,9 @@ +package org.apache.coyote.controller; + +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.response.Response; + +public interface Controller { + + void service(final Request request, final Response response); +} diff --git a/tomcat/src/main/java/org/apache/coyote/controller/ControllerMapper.java b/tomcat/src/main/java/org/apache/coyote/controller/ControllerMapper.java new file mode 100644 index 0000000000..73fc5a4a55 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/controller/ControllerMapper.java @@ -0,0 +1,29 @@ +package org.apache.coyote.controller; + +import org.apache.coyote.http11.request.Request; + +import java.util.HashMap; +import java.util.Map; + +public class ControllerMapper { + + private static final Controller STATIC_RESOURCE_CONTROLLER = new StaticResourceController(); + + private static final Map CONTROLLER_MAP = new HashMap<>(); + + private ControllerMapper() { + } + + public static void register(final String path, + final Controller controller) { + CONTROLLER_MAP.put(path, controller); + } + + public static Controller getController(final Request request) { + final String path = request.getRequestLine().getRequestPath(); + if (CONTROLLER_MAP.containsKey(path)) { + return CONTROLLER_MAP.get(path); + } + return STATIC_RESOURCE_CONTROLLER; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/controller/StaticResourceController.java b/tomcat/src/main/java/org/apache/coyote/controller/StaticResourceController.java new file mode 100644 index 0000000000..b0448bbb15 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/controller/StaticResourceController.java @@ -0,0 +1,56 @@ +package org.apache.coyote.controller; + +import org.apache.coyote.http11.request.Request; +import org.apache.coyote.http11.response.Response; + +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +import static org.apache.coyote.http11.response.StatusCode.*; + +public class StaticResourceController extends AbstractController { + + @Override + protected void doPost(final Request request, final Response response) { + response.setStatusCode(METHOD_NOT_ALLOWED); + } + + @Override + protected void doGet(final Request request, final Response response) { + final ClassLoader classLoader = getClass().getClassLoader(); + final String name = "static" + request.getRequestLine().getRequestPath(); + final URL fileURL = classLoader.getResource(name); + + if (fileURL == null) { + response.setStatusCode(NOT_FOUND); + response.writeStaticResource("/404.html"); + return; + } + + final URI fileURI; + try { + fileURI = fileURL.toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + + final StringBuilder stringBuilder = new StringBuilder(); + try (final InputStream inputStream = new FileInputStream(Paths.get(fileURI).toFile()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + + String nextLine; + while ((nextLine = bufferedReader.readLine()) != null) { + stringBuilder.append(nextLine) + .append(System.lineSeparator()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + + response.writeBody(stringBuilder.toString()); + } +} 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 7075be8e40..8de81c072c 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,27 +1,18 @@ 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.controller.Controller; +import org.apache.coyote.controller.ControllerMapper; import org.apache.coyote.http11.request.Request; -import org.apache.coyote.http11.request.RequestParameters; -import org.apache.coyote.http11.request.Session; import org.apache.coyote.http11.response.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.net.Socket; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Paths; -import java.util.Optional; - -import static org.apache.coyote.http11.request.RequestMethod.GET; -import static org.apache.coyote.http11.request.RequestMethod.POST; -import static org.apache.coyote.http11.response.Response.getUnauthorizedResponse; public class Http11Processor implements Runnable, Processor { @@ -48,104 +39,20 @@ public void process(final Socket connection) { final Request request = Request.from(bufferedReader); - final Response response = handle(request); - - response.decideHeaders(request); - - outputStream.write(response.parseString().getBytes()); - outputStream.flush(); - } catch (IOException | UncheckedServletException | URISyntaxException e) { - log.error(e.getMessage(), e); - } - } - - private Response handle(final Request request) throws URISyntaxException, IOException { - final String requestPath = request.getRequestLine().getRequestPath(); - if (requestPath.contains(".")) { - return findStaticResource(requestPath); - } - return mapPath(request); - } + final Response response = new Response(); - private Response mapPath(final Request request) throws IOException, URISyntaxException { - final RequestParameters requestParameters = request.getRequestParameters(); - final Session session = request.getSession(); + response.preprocess(request); - if (request.isMatching("/", GET)) { - return new Response("Hello world!"); - } - - if (request.isMatching("/login", GET)) { - final User user = (User) session.getAttribute("user"); - if (user != null) { - return Response.getRedirectResponse("/index.html"); - } - return findStaticResource("/login.html"); - } + final Controller controller = ControllerMapper.getController(request); - if (request.isMatching("/login", POST)) { - final String account = requestParameters.getValue("account"); - System.out.println("account = " + account); - if (account == null) { - return findStaticResource("/login.html"); - } + controller.service(request, response); - final Optional maybeUser = InMemoryUserRepository.findByAccount(account); - if (maybeUser.isEmpty()) { - return getUnauthorizedResponse(); - } - final User findUser = maybeUser.get(); - if (!findUser.checkPassword(requestParameters.getValue("password"))) { - return getUnauthorizedResponse(); - } - log.info("user: {}", findUser); + response.postprocess(request); - session.setAttribute("user", findUser); - - return Response.getRedirectResponse("/index.html"); - } - - if (request.isMatching("/register", GET)) { - return findStaticResource("/register.html"); - } - - if (request.isMatching("/register", POST)) { - final String account = requestParameters.getValue("account"); - final String password = requestParameters.getValue("password"); - final String email = requestParameters.getValue("email"); - - final User user = new User(account, password, email); - InMemoryUserRepository.save(user); - - return Response.getRedirectResponse("/login"); - } - - return Response.getNotFoundResponse(); - } - - private Response findStaticResource(final String requestUri) throws IOException, URISyntaxException { - final ClassLoader classLoader = getClass().getClassLoader(); - final String name = "static" + requestUri; - final URL fileURL = classLoader.getResource(name); - - if (fileURL == null) { - return Response.getNotFoundResponse(); - } - - final URI fileURI = fileURL.toURI(); - - final StringBuilder stringBuilder = new StringBuilder(); - try (final InputStream inputStream = new FileInputStream(Paths.get(fileURI).toFile()); - final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); - final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { - - String nextLine; - while ((nextLine = bufferedReader.readLine()) != null) { - stringBuilder.append(nextLine) - .append(System.lineSeparator()); - } + outputStream.write(response.parseString().getBytes()); + outputStream.flush(); + } catch (Exception e) { + log.error(e.getMessage(), e); } - - return new Response(stringBuilder.toString()); } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/header/Headers.java b/tomcat/src/main/java/org/apache/coyote/http11/header/Headers.java index 4ffee548e3..3b11775342 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/header/Headers.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/header/Headers.java @@ -1,9 +1,5 @@ package org.apache.coyote.http11.header; -import org.apache.coyote.http11.request.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -11,19 +7,19 @@ public class Headers { - private static final Logger log = LoggerFactory.getLogger(Request.class); - - private final Map headers = new LinkedHashMap<>(); + private final Map values = new LinkedHashMap<>(); public void addHeader(final Header header, - final String value) { - headers.put(header, value); + final String value) { + this.values.put(header, value); } public String parseResponse() { final List stringHeaders = new ArrayList<>(); - for (Header header : headers.keySet()) { - final String format = String.format("%s: %s ", header.getValue(), headers.get(header)); + for (Map.Entry headerStringEntry : values.entrySet()) { + final Header key = headerStringEntry.getKey(); + final String value = headerStringEntry.getValue(); + final String format = String.format("%s: %s ", key.getValue(), value); stringHeaders.add(format); } return String.join("\r\n", stringHeaders); @@ -42,7 +38,7 @@ public void addRequestHeaders(final List requestHeaderLines) { private void addRequestHeader(final String requestHeaderName, final String requestHeaderValue) { try { - headers.put(RequestHeader.from(requestHeaderName), requestHeaderValue); + values.put(RequestHeader.from(requestHeaderName), requestHeaderValue); } catch (RuntimeException e) { addEntityHeader(requestHeaderName, requestHeaderValue); } @@ -51,13 +47,20 @@ private void addRequestHeader(final String requestHeaderName, private void addEntityHeader(final String requestHeaderName, final String requestHeaderValue) { try { - headers.put(EntityHeader.from(requestHeaderName), requestHeaderValue); + values.put(EntityHeader.from(requestHeaderName), requestHeaderValue); } catch (RuntimeException ignored) { } } public String getValue(final Header header) { - return headers.getOrDefault(header, ""); + return values.getOrDefault(header, ""); + } + + @Override + public String toString() { + return "Headers{" + + "headers=" + values + + '}'; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/header/ResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/http11/header/ResponseHeader.java index 86081c9bb1..1d68fb2976 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/header/ResponseHeader.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/header/ResponseHeader.java @@ -4,8 +4,7 @@ public enum ResponseHeader implements Header { LOCATION("Location"), SET_COOKIE("Set-Cookie"), - EXPIRES("Expires") - ; + EXPIRES("Expires"); final String value; diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Cookie.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Cookie.java new file mode 100644 index 0000000000..0794693e96 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Cookie.java @@ -0,0 +1,55 @@ +package org.apache.coyote.http11.request; + +import java.util.HashMap; +import java.util.Map; + +public class Cookie { + + public static final Cookie EMPTY_COOKIE = new Cookie("", ""); + + private final String key; + + private final String value; + + + public Cookie(final String key, + final String value) { + this.key = key; + this.value = value; + } + + public static Map getCookies(final String cookieHeaderValue) { + final Map httpCookie = new HashMap<>(); + + if (cookieHeaderValue == null || cookieHeaderValue.equals("")) { + return httpCookie; + } + + final String[] cookieEntries = cookieHeaderValue.split(";"); + for (String cookieEntry : cookieEntries) { + final String[] cookieNameAndValue = cookieEntry.trim().split("="); + final String cookieKey = cookieNameAndValue[0].toLowerCase(); + final String cookieValue = cookieNameAndValue[1].toLowerCase(); + final Cookie cookie = new Cookie(cookieKey, cookieValue); + httpCookie.put(cookieKey, cookie); + } + return httpCookie; + + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "Cookie{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java b/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java deleted file mode 100644 index d254070d4b..0000000000 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/HttpCookie.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.apache.coyote.http11.request; - -import java.util.HashMap; -import java.util.Map; - -public class HttpCookie { - - private final Map httpCookie; - - public HttpCookie() { - this(new HashMap<>()); - } - - private HttpCookie(final Map httpCookie) { - this.httpCookie = httpCookie; - } - - public static HttpCookie from(final String cookieHeader) { - final Map httpCookie = new HashMap<>(); - - if (cookieHeader == null || cookieHeader.equals("")) { - return new HttpCookie(httpCookie); - } - - final String[] cookies = cookieHeader.split(";"); - for (String cookie : cookies) { - final String[] cookieNameAndValue = cookie.trim().split("="); - final String cookieName = cookieNameAndValue[0].toLowerCase(); - final String cookieValue = cookieNameAndValue[1].toLowerCase(); - httpCookie.put(cookieName, cookieValue); - } - return new HttpCookie(httpCookie); - } - - public boolean existCookie(final String name) { - return httpCookie.keySet().stream() - .anyMatch(key -> key.equalsIgnoreCase(name)); - } - - public String parseSetCookieHeader(final String name) { - final String value = httpCookie.get(name); - return String.format("%s=%s; ", name, value); - } - - public void addCookie(final String key, final String value) { - httpCookie.put(key.toLowerCase(), value); - } - - public String findCookie(final String key) { - return httpCookie.getOrDefault(key.toLowerCase(), ""); - } -} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java index 039e918f54..224d27432e 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Request.java @@ -6,67 +6,56 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Optional; import static org.apache.coyote.http11.header.EntityHeader.CONTENT_LENGTH; -import static org.apache.coyote.http11.header.EntityHeader.CONTENT_TYPE; import static org.apache.coyote.http11.header.RequestHeader.COOKIE; public class Request { + public static final String SESSION_ID_KEY = "jsessionid"; private final RequestLine requestLine; private final Headers headers; private final RequestParameters requestParameters; - private final HttpCookie httpCookie; + private final Map cookies; private final Session session; private final String body; - public Request(final RequestLine requestLine, - final Headers headers, - final RequestParameters requestParameters, - final HttpCookie httpCookie, - final Session session, - final String body) { + private Request(final RequestLine requestLine, + final Headers headers, + final RequestParameters requestParameters, + final Map cookies, + final Session session, + final String body) { this.requestLine = requestLine; this.headers = headers; this.requestParameters = requestParameters; - this.httpCookie = httpCookie; + this.cookies = cookies; this.session = session; this.body = body; } public static Request from(final BufferedReader bufferedReader) throws IOException { final List requestHeaderLines = getHeader(bufferedReader); - - final String requestFirstLine = requestHeaderLines.get(0); - final RequestLine requestLine = RequestLine.from(requestFirstLine); + final RequestLine requestLine = RequestLine.from(requestHeaderLines.get(0)); final Headers headers = new Headers(); headers.addRequestHeaders(requestHeaderLines); final String body = getBody(bufferedReader, headers); - final String queryStrings = extractQueryStrings(requestFirstLine, body, headers); - final RequestParameters requestParameters = RequestParameters.from(queryStrings); - - final String cookieHeader = headers.getValue(COOKIE); - final HttpCookie httpCookie = HttpCookie.from(cookieHeader); + final RequestParameters requestParameters = RequestParameters.of(requestLine, headers, body); - final String jsessionid = httpCookie.findCookie("JSESSIONID"); - final Session session = SessionManager.findSession(jsessionid); + final Map cookies = Cookie.getCookies(headers.getValue(COOKIE)); - return new Request(requestLine, headers, requestParameters, httpCookie, session, body); - } + final Session session = SessionManager.findSession(cookies.getOrDefault(SESSION_ID_KEY.toLowerCase(), Cookie.EMPTY_COOKIE).getValue()); - private static String getBody(final BufferedReader bufferedReader, final Headers headers) throws IOException { - final String contentLengthValue = headers.getValue(CONTENT_LENGTH) ; - final int contentLength = "".equals(contentLengthValue) ? 0 : Integer.parseInt(contentLengthValue); - final char[] buffer = new char[contentLength]; - bufferedReader.read(buffer, 0, contentLength); - return new String(buffer); + return new Request(requestLine, headers, requestParameters, cookies, session, body); } private static List getHeader(final BufferedReader bufferedReader) throws IOException { @@ -74,34 +63,28 @@ private static List getHeader(final BufferedReader bufferedReader) throw String nextLine; while (!"".equals(nextLine = bufferedReader.readLine())) { if (nextLine == null) { - throw new RuntimeException("헤더가 잘못되었습니다."); + throw new IllegalStateException("헤더가 잘못되었습니다."); } requestHeaderLines.add(nextLine); } return requestHeaderLines; } - private static String extractQueryStrings(final String requestFirstLine, - final String body, - final Headers headers) { - - final String[] requestFirstLineElements = requestFirstLine.split(" "); - final String requestUriValue = requestFirstLineElements[1]; - - if (requestUriValue.contains("?")) { - return requestUriValue.substring(requestUriValue.indexOf("?") + 1); - } - - if ("application/x-www-form-urlencoded".equalsIgnoreCase(headers.getValue(CONTENT_TYPE))) { - return body; - } - return ""; + private static String getBody(final BufferedReader bufferedReader, final Headers headers) throws IOException { + final String contentLengthValue = headers.getValue(CONTENT_LENGTH); + final int contentLength = "".equals(contentLengthValue) ? 0 : Integer.parseInt(contentLengthValue); + final char[] buffer = new char[contentLength]; + bufferedReader.read(buffer, 0, contentLength); + return new String(buffer); } - public boolean isMatching(final String requestPath, final RequestMethod requestMethod) { - return requestLine.isMatching(requestPath, requestMethod); + public Optional getParameter(final String parameterKey) { + return requestParameters.getValue(parameterKey); } + public Optional getCookie(final String key) { + return Optional.ofNullable(cookies.get(key)); + } public RequestLine getRequestLine() { return requestLine; @@ -111,14 +94,6 @@ public Headers getHeaders() { return headers; } - public RequestParameters getRequestParameters() { - return requestParameters; - } - - public HttpCookie getHttpCookie() { - return httpCookie; - } - public Session getSession() { return session; } @@ -133,7 +108,7 @@ public String toString() { "requestLine=" + requestLine + ", headers=" + headers + ", requestParameters=" + requestParameters + - ", httpCookie=" + httpCookie + + ", cookies=" + cookies + ", body='" + body + '\'' + '}'; } 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 index 8c376c5d67..5f44ef2e6d 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestLine.java @@ -5,41 +5,28 @@ public class RequestLine { private final RequestMethod requestMethod; - private final String requestPath; + private final RequestUri requestUri; private final Protocol protocol; - private RequestLine(final RequestMethod requestMethod, - final String requestPath, + public RequestLine(final RequestMethod requestMethod, + final RequestUri requestUri, final Protocol protocol) { this.requestMethod = requestMethod; - this.requestPath = requestPath; + this.requestUri = requestUri; this.protocol = protocol; } public static RequestLine from(final String requestFirstLine) { - final String[] requestFirstLineElements = requestFirstLine.split(" "); final String requestMethodValue = requestFirstLineElements[0]; final String requestUriValue = requestFirstLineElements[1]; final String requestProtocolValue = requestFirstLineElements[2]; - final String path = getPath(requestUriValue); final RequestMethod requestMethod = RequestMethod.from(requestMethodValue); + final RequestUri requestUri = RequestUri.from(requestUriValue); final Protocol protocol = Protocol.from(requestProtocolValue); - return new RequestLine(requestMethod, path, protocol); - } - - private static String getPath(final String requestUri) { - if (requestUri.contains("?")) { - return requestUri.substring(0, requestUri.indexOf("?")); - } - return requestUri; - } - - public boolean isMatching(final String requestPath, final RequestMethod requestMethod) { - return this.requestPath.equals(requestPath) - && this.requestMethod == requestMethod; + return new RequestLine(requestMethod, requestUri, protocol); } public RequestMethod getRequestMethod() { @@ -47,7 +34,11 @@ public RequestMethod getRequestMethod() { } public String getRequestPath() { - return requestPath; + return requestUri.getPath(); + } + + public String getQueryString() { + return requestUri.getQueryString(); } public Protocol getProtocol() { @@ -58,7 +49,7 @@ public Protocol getProtocol() { public String toString() { return "RequestLine{" + "requestMethod=" + requestMethod + - ", requestPath='" + requestPath + '\'' + + ", requestUri='" + requestUri + ", protocol=" + protocol + '}'; } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestParameters.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestParameters.java index 4cb230de40..4635883fbe 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestParameters.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestParameters.java @@ -1,22 +1,32 @@ package org.apache.coyote.http11.request; +import org.apache.coyote.http11.header.Headers; + import java.util.HashMap; import java.util.Map; +import java.util.Optional; + +import static org.apache.coyote.http11.header.EntityHeader.CONTENT_TYPE; public class RequestParameters { - private final Map requestParameters; + public static final String FORM_DATA_CONTENT_TYPE = "application/x-www-form-urlencoded"; - private RequestParameters(final Map requestParameters) { - this.requestParameters = requestParameters; + private final Map values; + + private RequestParameters(final Map values) { + this.values = values; } - public static RequestParameters from(final String queryStrings) { + public static RequestParameters of(final RequestLine requestLine, + final Headers headers, + final String body) { + final String requestParameters = extractRequestParameters(requestLine, headers, body); final Map requestQueryParameters = new HashMap<>(); - if (queryStrings == null || "".equals(queryStrings)){ + if (requestParameters == null || "".equals(requestParameters)) { return new RequestParameters(requestQueryParameters); } - final String[] queryStringsNameAndValue = queryStrings.split("&"); + final String[] queryStringsNameAndValue = requestParameters.split("&"); for (String queryString : queryStringsNameAndValue) { final String[] queryStringNameAndValue = queryString.split("="); final String name = queryStringNameAndValue[0]; @@ -26,18 +36,32 @@ public static RequestParameters from(final String queryStrings) { return new RequestParameters(requestQueryParameters); } - public String getValue(final String key) { - return requestParameters.get(key); + private static String extractRequestParameters(final RequestLine requestLine, + final Headers headers, + final String body) { + final String queryString = requestLine.getQueryString(); + if (queryString != null && !"".equals(queryString)) { + return queryString; + } + String value = headers.getValue(CONTENT_TYPE); + if (FORM_DATA_CONTENT_TYPE.equalsIgnoreCase(value)) { + return body; + } + return ""; + } + + public Optional getValue(final String key) { + return Optional.ofNullable(values.get(key)); } - public Map getRequestParameters() { - return requestParameters; + public Map getValues() { + return values; } @Override public String toString() { return "RequestParameters{" + - "requestParameters=" + requestParameters + + "requestParameters=" + values + '}'; } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java new file mode 100644 index 0000000000..2b5f03d2a7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/RequestUri.java @@ -0,0 +1,40 @@ +package org.apache.coyote.http11.request; + +public class RequestUri { + + public static final String SEPARATOR = "?"; + + private final String path; + + private final String queryString; + + private RequestUri(final String path, + final String queryString) { + this.path = path; + this.queryString = queryString; + } + + public static RequestUri from(final String requestUri) { + if (requestUri.contains(SEPARATOR)) { + String[] split = requestUri.split("\\" + SEPARATOR); + return new RequestUri(split[0], split[1]); + } + return new RequestUri(requestUri, ""); + } + + public String getPath() { + return path; + } + + public String getQueryString() { + return queryString; + } + + @Override + public String toString() { + return "RequestUri{" + + "path='" + path + '\'' + + ", queryString='" + queryString + '\'' + + '}'; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/request/Session.java b/tomcat/src/main/java/org/apache/coyote/http11/request/Session.java index 09ee80804a..0109fc5739 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/request/Session.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/request/Session.java @@ -1,22 +1,19 @@ package org.apache.coyote.http11.request; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class Session { private final String id; - private final Map value = new HashMap<>(); - private LocalDateTime expiredAt; + private final Map value = new ConcurrentHashMap<>(); + private final LocalDateTime expiredAt; public Session() { this(UUID.randomUUID().toString(), LocalDateTime.now().plusDays(1)); } - public Session(final String id) { - this(id, LocalDateTime.now().plusDays(1)); - } public Session(final String id, final LocalDateTime expiredAt) { @@ -25,7 +22,6 @@ public Session(final String id, } - public Object getAttribute(final String name) { validate(); return value.get(name); @@ -36,15 +32,7 @@ public void setAttribute(final String name, final Object value) { this.value.put(name, value); } - public void removeAttribute(final String name) { - value.remove(name); - } - - public void invalidate() { - expiredAt = LocalDateTime.now(); - } - - private void validate(){ + private void validate() { if (expiredAt.isBefore(LocalDateTime.now())) { throw new RuntimeException("유효기간이 지난 세션입니다"); } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java b/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java index 086c33c756..53abe3b392 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/Response.java @@ -1,51 +1,57 @@ package org.apache.coyote.http11.response; -import org.apache.coyote.http11.header.EntityHeader; +import org.apache.coyote.http11.header.Header; import org.apache.coyote.http11.header.Headers; import org.apache.coyote.http11.header.RequestHeader; +import org.apache.coyote.http11.request.Cookie; import org.apache.coyote.http11.request.Request; import org.apache.coyote.http11.request.Session; -import org.apache.coyote.http11.request.SessionManager; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.apache.coyote.http11.header.EntityHeader.CONTENT_LENGTH; import static org.apache.coyote.http11.header.EntityHeader.CONTENT_TYPE; import static org.apache.coyote.http11.header.ResponseHeader.LOCATION; import static org.apache.coyote.http11.header.ResponseHeader.SET_COOKIE; -import static org.apache.coyote.http11.response.StatusCode.FOUND; +import static org.apache.coyote.http11.response.StatusCode.*; public class Response { private final StatusLine statusLine; - private final Headers headers; - private final String body; - public Response(final String body) { - this(StatusLine.DEFAULT_STATUS_LINE, new Headers(), body); - } + private Headers headers; - public Response(final StatusLine statusLine, - final Headers headers, - final String body) { - this.statusLine = statusLine; - this.headers = headers; - this.body = body; - } + private final Map cookies; - public static Response getRedirectResponse(final String path) { - final Headers redirectHeaders = new Headers(); - redirectHeaders.addHeader(LOCATION, path); - return new Response(new StatusLine(FOUND), redirectHeaders, ""); + private String body; + + public Response() { + this(new StatusLine(), new Headers(), ""); } - public static Response getNotFoundResponse() { - final Headers notFoundHeaders = new Headers(); - notFoundHeaders.addHeader(LOCATION, "/404.html"); - return new Response(new StatusLine(FOUND), notFoundHeaders, ""); + private Response(final StatusLine statusLine, + final Headers headers, + final String body) { + this(statusLine, headers, new HashMap<>(), body); } - public static Response getUnauthorizedResponse() { - final Headers unauthorizedHeaders = new Headers(); - unauthorizedHeaders.addHeader(LOCATION, "/401.html"); - return new Response(new StatusLine(FOUND), unauthorizedHeaders, ""); + private Response(final StatusLine statusLine, + final Headers headers, + final Map cookies, + final String body) { + this.statusLine = statusLine; + this.headers = headers; + this.cookies = cookies; + this.body = body; } public String parseString() { @@ -56,33 +62,89 @@ public String parseString() { body); } - public void decideHeaders(final Request request) { - decideSetSession(request); - decideContentType(request); + public void addCookie(final Cookie cookie) { + cookies.put(cookie.getKey(), cookie); + + final String setCookieValue = cookies.keySet().stream() + .map(key -> key + "=" + cookies.get(key).getValue()) + .collect(Collectors.joining("; ")); + + headers.addHeader(SET_COOKIE, setCookieValue); + } + + public void writeBody(final String content) { + final String contentLength = String.valueOf(content.length()); + headers.addHeader(CONTENT_LENGTH, contentLength); + + this.body = content; + } + + public void writeStaticResource(final String path) { + final ClassLoader classLoader = getClass().getClassLoader(); + final String name = "static" + path; + final URL fileURL = classLoader.getResource(name); + + if (fileURL == null) { + throw new IllegalStateException("정적 파일이 존재하지 않습니다"); + } + + final URI fileURI; + try { + fileURI = fileURL.toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException("경로 문제가 발생했습니다"); + } + + final StringBuilder stringBuilder = new StringBuilder(); + try (final InputStream inputStream = new FileInputStream(Paths.get(fileURI).toFile()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + + String nextLine; + while ((nextLine = bufferedReader.readLine()) != null) { + stringBuilder.append(nextLine) + .append(System.lineSeparator()); + } + } catch (IOException e) { + throw new IllegalStateException("파일을 읽는 중 문제가 발생했습니다."); + } + + this.body = stringBuilder.toString(); decideContentLength(); } - private void decideSetSession(final Request request) { + private void decideContentLength() { + final byte[] bytes = body.getBytes(); + headers.addHeader(CONTENT_LENGTH, String.valueOf(bytes.length)); + } + + public void addHeader(final Header header, final String value) { + headers.addHeader(header, value); + } + + public void postprocess(final Request request) { final Session session = request.getSession(); - if (session.isAvailable()) { - headers.addHeader(SET_COOKIE, "JSESSIONID=" + session.getId()); - return; + final String sessionId = session.getId(); + final Optional sessionIdCookie = request.getCookie("JSESSIONID".toLowerCase()); + if (sessionIdCookie.isEmpty() || !Objects.equals(sessionIdCookie.get().getValue(), sessionId)) { + final Cookie cookie = new Cookie("JSESSIONID", session.getId()); + addCookie(cookie); } - SessionManager.remove(session); } - private void decideContentType(final Request request) { + public void preprocess(final Request request) { final String acceptHeaderValue = request.getHeaders().getValue(RequestHeader.ACCEPT); final String requestPath = request.getRequestLine().getRequestPath(); final String contentTypeValue = decideResponseContentType(acceptHeaderValue, requestPath); headers.addHeader(CONTENT_TYPE, contentTypeValue); } + private String decideResponseContentType(final String requestAcceptHeader, final String requestUri) { String responseFileExtension = requestUri.substring(requestUri.indexOf(".") + 1); if ("text/css".equals(requestAcceptHeader) || "css".equals(responseFileExtension)) { - return "text/css,*/*;q=0.1"; + return "text/css,*/*;q=0.1"; } if ("application/javascript".equals(requestAcceptHeader) || "js".equals(responseFileExtension)) { return "application/javascript;charset=utf-8"; @@ -90,9 +152,11 @@ private String decideResponseContentType(final String requestAcceptHeader, return "text/html;charset=utf-8"; } - private void decideContentLength() { - final byte[] bytes = body.getBytes(); - headers.addHeader(EntityHeader.CONTENT_LENGTH, String.valueOf(bytes.length)); + public void redirect(final String path) { + this.statusLine.setStatusCode(FOUND); + final Headers redirectHeaders = new Headers(); + redirectHeaders.addHeader(LOCATION, path); + this.headers = redirectHeaders; } public StatusLine getStatusLine() { @@ -107,11 +171,16 @@ public String getBody() { return body; } + public void setStatusCode(final StatusCode statusCode) { + statusLine.setStatusCode(statusCode); + } + @Override public String toString() { return "Response{" + "statusLine=" + statusLine + ", headers=" + headers + + ", cookies=" + cookies + ", body='" + body + '\'' + '}'; } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java index c35d57984c..eb0fcb1e36 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusCode.java @@ -3,9 +3,11 @@ public enum StatusCode { OK(200, "OK"), + CREATED(201, "CREATED"), FOUND(302, "FOUND"), UNAUTHORIZED(401, "UNAUTHORIZED"), NOT_FOUND(404, "NOT FOUND"), + METHOD_NOT_ALLOWED(405, "Method Not Allowed"), ; final int code; 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 index 6382ca4c0f..89747484d0 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/response/StatusLine.java @@ -4,13 +4,11 @@ public class StatusLine { - public static final StatusLine DEFAULT_STATUS_LINE = new StatusLine(StatusCode.OK); - private final Protocol protocol; - private final StatusCode statusCode; + private StatusCode statusCode; - public StatusLine(final StatusCode statusCode) { - this(Protocol.HTTP11, statusCode); + public StatusLine() { + this(Protocol.HTTP11, StatusCode.OK); } public StatusLine(final Protocol protocol, final StatusCode statusCode) { @@ -18,14 +16,14 @@ public StatusLine(final Protocol protocol, final StatusCode statusCode) { this.statusCode = statusCode; } - public Protocol getProtocol() { - return protocol; - } - public StatusCode getStatusCode() { return statusCode; } + public void setStatusCode(final StatusCode statusCode) { + this.statusCode = statusCode; + } + public String parseResponse() { final StringBuilder stringBuilder = new StringBuilder(); 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 3623e98085..12ad037442 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,6 +1,9 @@ package nextstep.org.apache.coyote.http11; +import nextstep.jwp.controller.HomeController; +import org.apache.coyote.controller.ControllerMapper; import org.apache.coyote.http11.Http11Processor; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import support.StubSocket; @@ -9,13 +12,16 @@ import java.net.URL; import java.nio.file.Files; +import static org.apache.coyote.http11.header.EntityHeader.CONTENT_LENGTH; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; class Http11ProcessorTest { @Test void process() { // given + ControllerMapper.register("/", new HomeController()); final var socket = new StubSocket(); final var processor = new Http11Processor(socket); @@ -23,14 +29,11 @@ void process() { processor.process(socket); // then - var expected = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: 12 ", - "", - "Hello world!"); - - assertThat(socket.output()).isEqualTo(expected); + final String actual = socket.output(); + assertAll(() -> { + assertThat(actual).contains("HTTP/1.1 200 OK"); + assertThat(actual).contains("Content-Type: text/html;charset=utf-8"); + }); } @Test @@ -57,6 +60,11 @@ void index() throws IOException { "\r\n"+ new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - assertThat(socket.output()).isEqualTo(expected); + + final String actual = socket.output(); + assertAll(() -> { + assertThat(actual).contains("HTTP/1.1 200 OK"); + assertThat(actual).contains("Content-Type: text/html;charset=utf-8"); + }); } } diff --git a/tomcat/src/test/java/org/apache/coyote/http11/header/HeadersTest.java b/tomcat/src/test/java/org/apache/coyote/http11/header/HeadersTest.java new file mode 100644 index 0000000000..776f1019f0 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/header/HeadersTest.java @@ -0,0 +1,36 @@ +package org.apache.coyote.http11.header; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeadersTest { + + @Test + void 헤더_정보를_String으로_변환한다() { + //given + final Headers headers = new Headers(); + headers.addHeader(EntityHeader.CONTENT_TYPE, "text/html"); + headers.addHeader(EntityHeader.CONTENT_LENGTH, "100"); + + //when + final String actual = headers.parseResponse(); + + //then + final String expected = "Content-Type: text/html \r\n" + + "Content-Length: 100 "; + assertThat(actual).isEqualTo(expected); + } + + @Test + void 헤더_정보를_추가한다() { + //given + final Headers headers = new Headers(); + + //when + headers.addHeader(EntityHeader.CONTENT_TYPE, "text/html"); + + //then + assertThat(headers.getValue(EntityHeader.CONTENT_TYPE)).isEqualTo("text/html"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java new file mode 100644 index 0000000000..c340e696c8 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestLineTest.java @@ -0,0 +1,25 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.protocol.Protocol; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RequestLineTest { + + @Test + void HTTP_요청_헤더_첫_라인의_문자열을_받아서_생성한다() { + // given + final String firstLine = "GET /index.html HTTP/1.1 "; + + // when + final RequestLine requestLine = RequestLine.from(firstLine); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(requestLine.getRequestPath()).isEqualTo("/index.html"); + softAssertions.assertThat(requestLine.getRequestMethod()).isEqualTo(RequestMethod.GET); + softAssertions.assertThat(requestLine.getProtocol()).isEqualTo(Protocol.HTTP11); + }); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestParametersTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestParametersTest.java new file mode 100644 index 0000000000..9a1e62668d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestParametersTest.java @@ -0,0 +1,60 @@ +package org.apache.coyote.http11.request; + +import org.apache.coyote.http11.header.EntityHeader; +import org.apache.coyote.http11.header.Headers; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RequestParametersTest { + + @Test + void 리퀘스트라인_헤더_바디_정보를_받아서_생성한다_쿼리스트링() { + //given + final RequestLine requestLine = RequestLine.from("GET /login?a=1&b=2 HTTP/1.1 "); + final Headers headers = new Headers(); + final String body = ""; + + //when + final RequestParameters requestParameters = RequestParameters.of(requestLine, headers, body); + + //then + assertThat(requestParameters.getValues()).hasSize(2); + } + + @Test + void 리퀘스트라인_헤더_바디_정보를_받아서_생성한다_리퀘스트바디() { + //given + final String content = "a=1&b=2"; + final RequestLine requestLine = RequestLine.from("POST /login HTTP/1.1 "); + final Headers headers = new Headers(); + headers.addHeader(EntityHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); + headers.addHeader(EntityHeader.CONTENT_LENGTH, String.valueOf(content.length())); + final String body = "a=1&b=2"; + + //when + final RequestParameters requestParameters = RequestParameters.of(requestLine, headers, body); + + //then + assertThat(requestParameters.getValues()).hasSize(2); + } + + @Test + void 쿼리파라미터키값으로_조회한다() { + //given + final String content = "a=1&b=2"; + final RequestLine requestLine = RequestLine.from("POST /login HTTP/1.1 "); + final Headers headers = new Headers(); + headers.addHeader(EntityHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); + headers.addHeader(EntityHeader.CONTENT_LENGTH, String.valueOf(content.length())); + final String body = "a=1&b=2"; + final RequestParameters requestParameters = RequestParameters.of(requestLine, headers, body); + + //when & then + assertSoftly(softAssertions -> { + softAssertions.assertThat(requestParameters.getValue("a")).contains("1"); + softAssertions.assertThat(requestParameters.getValue("b")).contains("2"); + }); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java new file mode 100644 index 0000000000..075fd8babb --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestTest.java @@ -0,0 +1,126 @@ +package org.apache.coyote.http11.request; + +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RequestTest { + + @Test + void BufferedReader를_입력받아_생성한다() throws IOException { + // given + final String requestString = String.join("\r\n", + "GET /index.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + + // when + final Request request = Request.from(bufferedReader); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(request.getRequestLine().getRequestPath()).isEqualTo("/index.html"); + softAssertions.assertThat(request.getRequestLine().getRequestMethod()).isEqualTo(RequestMethod.GET); + softAssertions.assertThat(request.getBody()).isEmpty(); + }); + } + + @Test + void 키값으로_요청_파라미터값을_조회한다_쿼리스트링() throws IOException { + // given + final String requestString = String.join("\r\n", + "GET /index.html?a=1&b=2 HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + + // when + final Optional actual = request.getParameter("b"); + + // then + assertThat(actual).contains("2"); + } + + + @Test + void 키값으로_요청_파라미터값을_조회한다_리퀘스트바디() throws IOException { + // given + String content = "a=1&b=2"; + final String requestString = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Content-Type: application/x-www-form-urlencoded ", + "Content-Length: " + content.length(), + "", + content); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + + // when + final Optional actual = request.getParameter("b"); + + // then + assertThat(actual).contains("2"); + } + + @Test + void JSESSIONID가_있는_요청에서는_해당_세션을_조회하여_갖고있는다() throws IOException { + // given + final Session session = new Session(); + SessionManager.add(session); + + final String requestString = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "Cookie: JSESSIONID=" + session.getId() + " ", + "", + ""); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + + // when + Session actual = request.getSession(); + + // then + assertThat(actual).isEqualTo(session); + } + + @Test + void JSESSIONID가_없는_요청에서는_새로운_세션을_만들어서_갖고있는다() throws IOException { + // given + final String requestString = String.join("\r\n", + "POST /login HTTP/1.1 ", + "Host: localhost:8080 ", + "", + ""); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + + // when + final Session actual = request.getSession(); + + // then + assertThat(actual).isNotNull(); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/request/RequestUriTest.java b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestUriTest.java new file mode 100644 index 0000000000..cbb3967b1d --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/request/RequestUriTest.java @@ -0,0 +1,24 @@ +package org.apache.coyote.http11.request; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class RequestUriTest { + + @Test + void String_RequestURI_를_입력받아_객체를_생성하고_그_안에는_Path와_QueryString으로_나눠진다() { + //given + final String requestUriValue = "/path?a=1&b=2"; + + //when + final RequestUri requestUri = RequestUri.from(requestUriValue); + + //then + assertSoftly(softAssertions -> { + softAssertions.assertThat(requestUri.getPath()).isEqualTo("/path"); + softAssertions.assertThat(requestUri.getQueryString()).isEqualTo("a=1&b=2"); + }); + } + +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseTest.java new file mode 100644 index 0000000000..4c66bd3d23 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/ResponseTest.java @@ -0,0 +1,171 @@ +package org.apache.coyote.http11.response; + +import org.apache.coyote.http11.header.Headers; +import org.apache.coyote.http11.header.ResponseHeader; +import org.apache.coyote.http11.request.Cookie; +import org.apache.coyote.http11.request.Request; +import org.junit.jupiter.api.Test; + +import java.io.*; + +import static org.apache.coyote.http11.header.EntityHeader.CONTENT_LENGTH; +import static org.apache.coyote.http11.header.EntityHeader.CONTENT_TYPE; +import static org.apache.coyote.http11.response.StatusCode.UNAUTHORIZED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class ResponseTest { + + @Test + void 리다이렉트_응답을_생성한다() { + //given + final String path = "/abc"; + final Response redirectResponse = new Response(); + + //when + redirectResponse.redirect(path); + + //then + assertSoftly(softAssertions -> { + softAssertions.assertThat(redirectResponse.getStatusLine().getStatusCode()).isEqualTo(StatusCode.FOUND); + softAssertions.assertThat(redirectResponse.getHeaders().getValue(ResponseHeader.LOCATION)).isEqualTo(path); + }); + } + + @Test + void String으로_반환한다() { + //given + final Headers headers = new Headers(); + final String cookie = "a=b"; + final String body = "12345"; + final Response response = new Response(); + response.setStatusCode(StatusCode.OK); + response.addHeader(ResponseHeader.SET_COOKIE, cookie); + response.writeBody(body); + + //when + final String actual = response.parseString(); + + //then + assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).contains("HTTP/1.1 200 OK"); + softAssertions.assertThat(actual).contains("Set-Cookie: " + cookie); + softAssertions.assertThat(actual).contains(body); + }); + } + + @Test + void 바디에_String을_입력한다_그_때_컨텐트_길이_헤더도_결정된다() { + //given + final Response response = new Response(); + + //when + String content = "1234"; + response.writeBody(content); + + //then + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.getHeaders().getValue(CONTENT_LENGTH)).isEqualTo(String.valueOf(content.length())); + softAssertions.assertThat(response.getBody()).contains(content); + }); + } + + @Test + void 정적_페이지를_응답하다_그_때_컨텐트_길이_헤더도_결정된다() { + //given + final Response response = new Response(); + + //when + response.writeStaticResource("/index.html"); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("Content-Length: 5564"); + } + + @Test + void 헤더를_추가한다() { + //given + final Response response = new Response(); + + //when + response.addHeader(CONTENT_TYPE, "application/json"); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("Content-Type: application/json"); + } + + @Test + void 쿠키를_추가한다() { + //given + final Cookie cookie = new Cookie("a", "1"); + final Response response = new Response(); + + //when + response.addCookie(cookie); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("Set-Cookie: a=1"); + } + + @Test + void 응답_코드를_세팅한다() { + //given + final Response response = new Response(); + + //when + response.setStatusCode(UNAUTHORIZED); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("401 UNAUTHORIZED"); + } + + @Test + void Request를_보고_ContenType을_결정하는_전처리를_한다() throws IOException { + //given + final String requestString = String.join("\r\n", + "GET /index.html?a=1&b=2 HTTP/1.1 ", + "Host: localhost:8080 ", + "Accept: text/css ", + "", + ""); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + final Response response = new Response(); + + //when + response.preprocess(request); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("Content-Type: text/css"); + } + + @Test + void Request와_비교하여_JSSESIONID를_내려주는_후처리를_한다() throws IOException { + //given + final String requestString = String.join("\r\n", + "GET /index.html?a=1&b=2 HTTP/1.1 ", + "Host: localhost:8080 ", + "Accept: text/css ", + "", + ""); + final InputStream inputStream = new ByteArrayInputStream(requestString.getBytes()); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + final Request request = Request.from(bufferedReader); + final Response response = new Response(); + + //when + response.postprocess(request); + + //then + final String actual = response.parseString(); + assertThat(actual).contains("JSESSIONID"); + } +} diff --git a/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java b/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java new file mode 100644 index 0000000000..8b0d4f37f6 --- /dev/null +++ b/tomcat/src/test/java/org/apache/coyote/http11/response/StatusLineTest.java @@ -0,0 +1,21 @@ +package org.apache.coyote.http11.response; + +import org.apache.coyote.protocol.Protocol; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class StatusLineTest { + + @Test + void String으로_변환한다() { + //given + final StatusLine statusLine = new StatusLine(Protocol.HTTP11, StatusCode.OK); + + //when + final String actual = statusLine.parseResponse(); + + //then + assertThat(actual).isEqualTo("HTTP/1.1 200 OK "); + } +}