diff --git a/tomcat/src/main/java/nextstep/Application.java b/tomcat/src/main/java/nextstep/Application.java index 3dd7593507..19fea8514f 100644 --- a/tomcat/src/main/java/nextstep/Application.java +++ b/tomcat/src/main/java/nextstep/Application.java @@ -1,11 +1,28 @@ package nextstep; +import java.util.Set; + +import org.apache.catalina.HandlerMapping; import org.apache.catalina.startup.Tomcat; +import nextstep.jwp.handler.BaseRequestHandler; +import nextstep.jwp.handler.LoginRequestHandler; +import nextstep.jwp.handler.RegisterRequestHandler; +import nextstep.jwp.handler.StaticContentRequestHandler; + public class Application { - public static void main(String[] args) { - final var tomcat = new Tomcat(); - tomcat.start(); - } + public static void main(String[] args) { + + final var handlers = Set.of( + new BaseRequestHandler(), + new LoginRequestHandler(), + new RegisterRequestHandler() + ); + final var defaultHandler = new StaticContentRequestHandler(); + final var handlerMapping = new HandlerMapping(handlers, defaultHandler); + + final var tomcat = new Tomcat(handlerMapping); + tomcat.start(); + } } diff --git a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java index 1ca30e8383..db49cec08d 100644 --- a/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java +++ b/tomcat/src/main/java/nextstep/jwp/db/InMemoryUserRepository.java @@ -1,27 +1,30 @@ package nextstep.jwp.db; -import nextstep.jwp.model.User; - import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import nextstep.jwp.model.User; public class InMemoryUserRepository { - private static final Map database = new ConcurrentHashMap<>(); + private static final Map database = new ConcurrentHashMap<>(); + private static final AtomicLong idCounter = new AtomicLong(); - static { - final User user = new User(1L, "gugu", "password", "hkkang@woowahan.com"); - database.put(user.getAccount(), user); - } + static { + final User user = new User(idCounter.incrementAndGet(), "gugu", "password", "hkkang@woowahan.com"); + database.put(user.getAccount(), user); + } - public static void save(User user) { - database.put(user.getAccount(), user); - } + private InMemoryUserRepository() { + } - public static Optional findByAccount(String account) { - return Optional.ofNullable(database.get(account)); - } + public static void save(User user) { + database.put(user.getAccount(), user.addId(idCounter.incrementAndGet())); + } - private InMemoryUserRepository() {} + public static Optional findByAccount(String account) { + return Optional.ofNullable(database.get(account)); + } } diff --git a/tomcat/src/main/java/nextstep/jwp/handler/BaseRequestHandler.java b/tomcat/src/main/java/nextstep/jwp/handler/BaseRequestHandler.java new file mode 100644 index 0000000000..b8fecd0438 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/handler/BaseRequestHandler.java @@ -0,0 +1,21 @@ +package nextstep.jwp.handler; + +import org.apache.catalina.RequestHandler; +import org.apache.coyote.MimeType; +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; + +public class BaseRequestHandler extends RequestHandler { + + private static final String REQUEST_PATH = "/"; + private static final String RESPONSE_BODY = "Hello world!"; + + public BaseRequestHandler() { + super(REQUEST_PATH); + } + + @Override + protected Response doGet(final Request request) { + return Response.ok(RESPONSE_BODY, MimeType.HTML); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/handler/LoginRequestHandler.java b/tomcat/src/main/java/nextstep/jwp/handler/LoginRequestHandler.java new file mode 100644 index 0000000000..6a7800971d --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/handler/LoginRequestHandler.java @@ -0,0 +1,83 @@ +package nextstep.jwp.handler; + +import org.apache.catalina.RequestHandler; +import org.apache.catalina.session.Session; +import org.apache.catalina.session.SessionManager; +import org.apache.coyote.Cookie; +import org.apache.coyote.MimeType; +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; + +public class LoginRequestHandler extends RequestHandler { + + private static final Logger log = LoggerFactory.getLogger(LoginRequestHandler.class); + + private static final String REQUEST_PATH = "/login"; + private static final String LOGIN_PAGE_PATH = "/login.html"; + private static final String REDIRECT_LOCATION = "/index.html"; + + public LoginRequestHandler() { + super(LoginRequestHandler.REQUEST_PATH); + } + + @Override + protected Response doGet(final Request request) { + if (isSessionExist(request)) { + return Response.redirect(REDIRECT_LOCATION); + } + return Response.ok(ResourceProvider.provide(LOGIN_PAGE_PATH), MimeType.fromPath(LOGIN_PAGE_PATH)); + } + + private boolean isSessionExist(final Request request) { + final var sessionId = request.findSession(); + if (sessionId == null) { + return false; + } + return SessionManager.findById(sessionId) != null; + } + + @Override + protected Response doPost(final Request request) { + final var account = request.findBodyField("account"); + final var password = request.findBodyField("password"); + return login(account, password); + } + + private Response login(final String account, final String password) { + if (isInvalidInput(account, password)) { + return Response.badRequest(); + } + + final var user = InMemoryUserRepository.findByAccount(account); + if (user.isEmpty() || !user.get().checkPassword(password)) { + return Response.unauthorized(); + } + + final var session = createSession(user.get()); + final var cookie = Cookie.session(session.getId()); + + log.info("[LOGIN SUCCESS] account: {}", account); + return Response.redirect(REDIRECT_LOCATION) + .addCookie(cookie); + } + + private boolean isInvalidInput(final String account, final String password) { + return isBlank(account) || isBlank(password); + } + + private boolean isBlank(final String value) { + return value == null || value.isBlank(); + } + + private Session createSession(final User user) { + final var session = Session.create(); + session.setAttribute("user", user); + SessionManager.add(session); + return session; + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/handler/RegisterRequestHandler.java b/tomcat/src/main/java/nextstep/jwp/handler/RegisterRequestHandler.java new file mode 100644 index 0000000000..7e7cf9906c --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/handler/RegisterRequestHandler.java @@ -0,0 +1,59 @@ +package nextstep.jwp.handler; + +import org.apache.catalina.RequestHandler; +import org.apache.coyote.MimeType; +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nextstep.jwp.db.InMemoryUserRepository; +import nextstep.jwp.model.User; + +public class RegisterRequestHandler extends RequestHandler { + + private static final Logger log = LoggerFactory.getLogger(RegisterRequestHandler.class); + + private static final String REQUEST_PATH = "/register"; + private static final String PAGE_PATH = "/register.html"; + + public RegisterRequestHandler() { + super(REQUEST_PATH); + } + + @Override + protected Response doGet(final Request request) { + return Response.ok(ResourceProvider.provide(PAGE_PATH), MimeType.fromPath(PAGE_PATH)); + } + + @Override + protected Response doPost(final Request request) { + final var account = request.findBodyField("account"); + final var password = request.findBodyField("password"); + final var email = request.findBodyField("email"); + + return register(account, password, email); + } + + private Response register(final String account, final String password, final String email) { + if (isInvalidInput(account, password, email) || isDuplicatedAccount(account)) { + return Response.badRequest(); + } + + InMemoryUserRepository.save(new User(account, password, email)); + log.info("[REGISTER SUCCESS] account: {}", account); + return Response.redirect("/index.html"); + } + + private boolean isInvalidInput(final String account, final String password, final String email) { + return isBlank(account) || isBlank(password) || isBlank(email); + } + + private boolean isBlank(final String value) { + return value == null || value.isBlank(); + } + + private boolean isDuplicatedAccount(final String account) { + return InMemoryUserRepository.findByAccount(account).isPresent(); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/handler/ResourceProvider.java b/tomcat/src/main/java/nextstep/jwp/handler/ResourceProvider.java new file mode 100644 index 0000000000..69a690462a --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/handler/ResourceProvider.java @@ -0,0 +1,31 @@ +package nextstep.jwp.handler; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class ResourceProvider { + + private static final String RESOURCE_ROOT_PATH = "static"; + + public static String provide(final String path) { + final var resource = ResourceProvider.class.getClassLoader() + .getResource(RESOURCE_ROOT_PATH + path); + + if (resource == null) { + throw new IllegalArgumentException("해당하는 자원을 찾을 수 없습니다."); + } + + final var file = new File(resource.getPath()).toPath(); + + if (Files.isRegularFile(file)) { + try { + return new String(Files.readAllBytes(file)); + } catch (IOException e) { + throw new IllegalArgumentException("파일을 읽을 수 없습니다."); + } + } + + throw new IllegalArgumentException("올바르지 않은 파일입니다."); + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/handler/StaticContentRequestHandler.java b/tomcat/src/main/java/nextstep/jwp/handler/StaticContentRequestHandler.java new file mode 100644 index 0000000000..0357757068 --- /dev/null +++ b/tomcat/src/main/java/nextstep/jwp/handler/StaticContentRequestHandler.java @@ -0,0 +1,34 @@ +package nextstep.jwp.handler; + +import org.apache.catalina.RequestHandler; +import org.apache.coyote.MimeType; +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StaticContentRequestHandler extends RequestHandler { + + private static final Logger log = LoggerFactory.getLogger(StaticContentRequestHandler.class); + private static final String REQUEST_PATH = "/*"; + + public StaticContentRequestHandler() { + super(REQUEST_PATH); + } + + @Override + public Response handle(final Request request) { + final String requestPath = request.getPath(); + if (requestPath == null) { + return Response.notFound(); + } + try { + final var responseBody = ResourceProvider.provide(requestPath); + final var mimeType = MimeType.fromPath(requestPath); + return Response.ok(responseBody, mimeType); + } catch (IllegalArgumentException e) { + log.warn("{}: {}", request.getPath(), e.getMessage()); + return Response.notFound(); + } + } +} diff --git a/tomcat/src/main/java/nextstep/jwp/model/User.java b/tomcat/src/main/java/nextstep/jwp/model/User.java index 4c2a2cd184..98d9efa2a1 100644 --- a/tomcat/src/main/java/nextstep/jwp/model/User.java +++ b/tomcat/src/main/java/nextstep/jwp/model/User.java @@ -26,13 +26,17 @@ public String getAccount() { return account; } + public User addId(Long id) { + return new User(id, account, password, email); + } + @Override public String toString() { return "User{" + - "id=" + id + - ", account='" + account + '\'' + - ", email='" + email + '\'' + - ", password='" + password + '\'' + - '}'; + "id=" + id + + ", account='" + account + '\'' + + ", email='" + email + '\'' + + ", password='" + password + '\'' + + '}'; } } diff --git a/tomcat/src/main/java/org/apache/catalina/HandlerMapping.java b/tomcat/src/main/java/org/apache/catalina/HandlerMapping.java new file mode 100644 index 0000000000..c2beffe672 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/HandlerMapping.java @@ -0,0 +1,35 @@ +package org.apache.catalina; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; + +public class HandlerMapping { + + private final Map handlers; + private final RequestHandler defaultHandler; + + public HandlerMapping(final Set handlers, final RequestHandler defaultHandler) { + this.handlers = handlers.stream() + .collect(Collectors.toMap( + RequestHandler::getRequestPath, + handler -> handler + )); + this.defaultHandler = defaultHandler; + } + + public Response handle(final Request request) { + final String requestPath = request.getPath(); + if (requestPath == null) { + return Response.notFound(); + } + final var handler = handlers.get(requestPath); + if (handler == null) { + return defaultHandler.handle(request); + } + return handler.handle(request); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/Manager.java b/tomcat/src/main/java/org/apache/catalina/Manager.java index e69410f6a9..9acfc48cff 100644 --- a/tomcat/src/main/java/org/apache/catalina/Manager.java +++ b/tomcat/src/main/java/org/apache/catalina/Manager.java @@ -1,9 +1,9 @@ package org.apache.catalina; -import jakarta.servlet.http.HttpSession; - import java.io.IOException; +import jakarta.servlet.http.HttpSession; + /** * A Manager manages the pool of Sessions that are associated with a * particular Container. Different Manager implementations may support @@ -24,33 +24,33 @@ */ public interface Manager { - /** - * Add this Session to the set of active Sessions for this Manager. - * - * @param session Session to be added - */ - void add(HttpSession session); + /** + * Add this Session to the set of active Sessions for this Manager. + * + * @param session Session to be added + */ + void add(HttpSession session); - /** - * Return the active Session, associated with this Manager, with the - * 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 - */ - HttpSession findSession(String id) throws IOException; + /** + * Return the active Session, associated with this Manager, with the + * 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 + */ + HttpSession findSession(String id) throws IOException; - /** - * Remove this Session from the active Sessions for this Manager. - * - * @param session Session to be removed - */ - void remove(HttpSession session); + /** + * Remove this Session from the active Sessions for this Manager. + * + * @param session Session to be removed + */ + void remove(HttpSession session); } diff --git a/tomcat/src/main/java/org/apache/catalina/RequestHandler.java b/tomcat/src/main/java/org/apache/catalina/RequestHandler.java new file mode 100644 index 0000000000..a4f7dca053 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/RequestHandler.java @@ -0,0 +1,35 @@ +package org.apache.catalina; + +import org.apache.coyote.HttpMethod; +import org.apache.coyote.request.Request; +import org.apache.coyote.response.Response; + +public abstract class RequestHandler { + + private final String requestPath; + + protected RequestHandler(final String requestPath) { + this.requestPath = requestPath; + } + + public Response handle(final Request request) { + if (request.hasMethod(HttpMethod.GET)) { + return doGet(request); + } else if (request.hasMethod(HttpMethod.POST)) { + return doPost(request); + } + return Response.notFound(); + } + + protected Response doGet(final Request request) { + return Response.notFound(); + } + + protected Response doPost(final Request request) { + return Response.notFound(); + } + + public String getRequestPath() { + return requestPath; + } +} 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..de87d0264b 100644 --- a/tomcat/src/main/java/org/apache/catalina/connector/Connector.java +++ b/tomcat/src/main/java/org/apache/catalina/connector/Connector.java @@ -1,95 +1,98 @@ package org.apache.catalina.connector; -import org.apache.coyote.http11.Http11Processor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.UncheckedIOException; import java.net.ServerSocket; import java.net.Socket; +import org.apache.catalina.HandlerMapping; +import org.apache.coyote.http11.Http11Processor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class Connector implements Runnable { - private static final Logger log = LoggerFactory.getLogger(Connector.class); - - private static final int DEFAULT_PORT = 8080; - private static final int DEFAULT_ACCEPT_COUNT = 100; - - private final ServerSocket serverSocket; - private boolean stopped; - - public Connector() { - this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT); - } - - public Connector(final int port, final int acceptCount) { - this.serverSocket = createServerSocket(port, acceptCount); - this.stopped = false; - } - - private ServerSocket createServerSocket(final int port, final int acceptCount) { - try { - final int checkedPort = checkPort(port); - final int checkedAcceptCount = checkAcceptCount(acceptCount); - return new ServerSocket(checkedPort, checkedAcceptCount); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - public void start() { - var thread = new Thread(this); - thread.setDaemon(true); - thread.start(); - stopped = false; - log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); - } - - @Override - public void run() { - // 클라이언트가 연결될때까지 대기한다. - while (!stopped) { - connect(); - } - } - - private void connect() { - try { - process(serverSocket.accept()); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private void process(final Socket connection) { - if (connection == null) { - return; - } - var processor = new Http11Processor(connection); - new Thread(processor).start(); - } - - public void stop() { - stopped = true; - try { - serverSocket.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - private int checkPort(final int port) { - final var MIN_PORT = 1; - final var MAX_PORT = 65535; - - if (port < MIN_PORT || MAX_PORT < port) { - return DEFAULT_PORT; - } - return port; - } - - private int checkAcceptCount(final int acceptCount) { - return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); - } + private static final Logger log = LoggerFactory.getLogger(Connector.class); + + private static final int DEFAULT_PORT = 8080; + private static final int DEFAULT_ACCEPT_COUNT = 100; + + private final ServerSocket serverSocket; + private final HandlerMapping handlerMapping; + private boolean stopped; + + public Connector(final HandlerMapping handlerMapping) { + this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, handlerMapping); + } + + private Connector(final int port, final int acceptCount, final HandlerMapping handlerMapping) { + this.handlerMapping = handlerMapping; + this.serverSocket = createServerSocket(port, acceptCount); + this.stopped = false; + } + + private ServerSocket createServerSocket(final int port, final int acceptCount) { + try { + final int checkedPort = checkPort(port); + final int checkedAcceptCount = checkAcceptCount(acceptCount); + return new ServerSocket(checkedPort, checkedAcceptCount); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void start() { + var thread = new Thread(this); + thread.setDaemon(true); + thread.start(); + stopped = false; + log.info("Web Application Server started {} port.", serverSocket.getLocalPort()); + } + + @Override + public void run() { + // 클라이언트가 연결될때까지 대기한다. + while (!stopped) { + connect(); + } + } + + private void connect() { + try { + process(serverSocket.accept()); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private void process(final Socket connection) { + if (connection == null) { + return; + } + var processor = new Http11Processor(connection, handlerMapping); + new Thread(processor).start(); + } + + public void stop() { + stopped = true; + try { + serverSocket.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private int checkPort(final int port) { + final var MIN_PORT = 1; + final var MAX_PORT = 65535; + + if (port < MIN_PORT || MAX_PORT < port) { + return DEFAULT_PORT; + } + return port; + } + + private int checkAcceptCount(final int acceptCount) { + return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT); + } } diff --git a/tomcat/src/main/java/org/apache/catalina/session/Session.java b/tomcat/src/main/java/org/apache/catalina/session/Session.java new file mode 100644 index 0000000000..c4e01bf23b --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/Session.java @@ -0,0 +1,40 @@ +package org.apache.catalina.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class Session { + + private final String id; + private final Map attributes = new HashMap<>(); + + public Session(final String id) { + this.id = id; + } + + public static Session create() { + final var id = UUID.randomUUID().toString(); + return new Session(id); + } + + public String getId() { + return id; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttribute(final String name, final Object value) { + attributes.put(name, value); + } + + public void removeAttribute(final String name) { + attributes.remove(name); + } + + public void invalidate() { + attributes.clear(); + } +} diff --git a/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java new file mode 100644 index 0000000000..ca00eea8c7 --- /dev/null +++ b/tomcat/src/main/java/org/apache/catalina/session/SessionManager.java @@ -0,0 +1,21 @@ +package org.apache.catalina.session; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager { + + private static final Map sessions = new ConcurrentHashMap<>(); + + public static void add(final Session session) { + sessions.put(session.getId(), session); + } + + public static Session findById(final String id) { + return sessions.get(id); + } + + public static void remove(final Session session) { + sessions.remove(session.getId()); + } +} 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..de112237a6 100644 --- a/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java +++ b/tomcat/src/main/java/org/apache/catalina/startup/Tomcat.java @@ -1,27 +1,34 @@ package org.apache.catalina.startup; +import java.io.IOException; + +import org.apache.catalina.HandlerMapping; import org.apache.catalina.connector.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - public class Tomcat { - private static final Logger log = LoggerFactory.getLogger(Tomcat.class); - - public void start() { - var connector = new Connector(); - connector.start(); - - try { - // make the application wait until we press any key. - System.in.read(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } finally { - log.info("web server stop."); - connector.stop(); - } - } + private static final Logger log = LoggerFactory.getLogger(Tomcat.class); + + private final HandlerMapping handlerMapping; + + public Tomcat(final HandlerMapping handlerMapping) { + this.handlerMapping = handlerMapping; + } + + public void start() { + var connector = new Connector(handlerMapping); + connector.start(); + + try { + // make the application wait until we press any key. + System.in.read(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + log.info("web server stop."); + connector.stop(); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/Cookie.java b/tomcat/src/main/java/org/apache/coyote/Cookie.java new file mode 100644 index 0000000000..45113c7366 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/Cookie.java @@ -0,0 +1,30 @@ +package org.apache.coyote; + +public class Cookie { + + private static final String SESSION_ID_KEY = "JSESSIONID"; + + private final String key; + private final String value; + + public Cookie(final String key, final String value) { + this.key = key; + this.value = value; + } + + public static Cookie session(final String sessionId) { + return new Cookie(SESSION_ID_KEY, sessionId); + } + + public boolean isSession() { + return SESSION_ID_KEY.equals(key); + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/HttpMethod.java b/tomcat/src/main/java/org/apache/coyote/HttpMethod.java new file mode 100644 index 0000000000..3c69056a67 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/HttpMethod.java @@ -0,0 +1,15 @@ +package org.apache.coyote; + +public enum HttpMethod { + GET, + POST, + ; + + public static HttpMethod from(String method) { + try { + return valueOf(method.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("지원하지 않는 HTTP 메서드입니다."); + } + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/HttpProtocolVersion.java b/tomcat/src/main/java/org/apache/coyote/HttpProtocolVersion.java new file mode 100644 index 0000000000..2f3e43fbae --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/HttpProtocolVersion.java @@ -0,0 +1,25 @@ +package org.apache.coyote; + +public enum HttpProtocolVersion { + HTTP11("HTTP/1.1"), + ; + + private final String version; + + HttpProtocolVersion(final String version) { + this.version = version; + } + + public static HttpProtocolVersion version(String version) { + for (final HttpProtocolVersion value : values()) { + if (value.version.equals(version)) { + return value; + } + } + throw new IllegalArgumentException("지원하지 않는 HTTP 프로토콜 버전입니다."); + } + + public String getVersion() { + return version; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/MimeType.java b/tomcat/src/main/java/org/apache/coyote/MimeType.java new file mode 100644 index 0000000000..70869629a3 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/MimeType.java @@ -0,0 +1,52 @@ +package org.apache.coyote; + +import java.util.Arrays; + +public enum MimeType { + HTML(".html", "text/html", true), + CSS(".css", "text/css", true), + JS(".js", "application/javascript", true), + SVG(".svg", "image/svg+xml", false), + ; + + private final String fileExtension; + private final String value; + private final boolean isText; + + MimeType(final String fileExtension, final String value, final boolean isText) { + this.fileExtension = fileExtension; + this.value = value; + this.isText = isText; + } + + public static MimeType fromPath(final String path) { + validatePath(path); + final var optionalMimeType = Arrays.stream(values()) + .filter(mimeType -> path.endsWith(mimeType.fileExtension)) + .findAny(); + if (optionalMimeType.isEmpty()) { + throw new IllegalArgumentException("지원하지 않는 파일 확장자입니다."); + } + return optionalMimeType.get(); + } + + private static void validatePath(final String path) { + if (path == null) { + throw new IllegalArgumentException("파일 경로는 null일 수 없습니다."); + } + if (!path.contains(".")) { + throw new IllegalArgumentException("올바르지 않은 파일 경로입니다."); + } + } + + public String getValue() { + if (!isText) { + return value; + } + return addCharsetParam(value); + } + + private String addCharsetParam(String type) { + return type + ";charset=utf-8"; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java index 7f1b2c7e96..9c773d32d6 100644 --- a/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java @@ -1,47 +1,48 @@ package org.apache.coyote.http11; -import nextstep.jwp.exception.UncheckedServletException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; + +import org.apache.catalina.HandlerMapping; import org.apache.coyote.Processor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.Socket; +import nextstep.jwp.exception.UncheckedServletException; public class Http11Processor implements Runnable, Processor { - private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); - - private final Socket connection; - - public Http11Processor(final Socket connection) { - this.connection = connection; - } - - @Override - public void run() { - log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); - process(connection); - } - - @Override - public void process(final Socket connection) { - try (final var inputStream = connection.getInputStream(); - final var outputStream = connection.getOutputStream()) { - - final var responseBody = "Hello world!"; - - final var response = String.join("\r\n", - "HTTP/1.1 200 OK ", - "Content-Type: text/html;charset=utf-8 ", - "Content-Length: " + responseBody.getBytes().length + " ", - "", - responseBody); - - outputStream.write(response.getBytes()); - outputStream.flush(); - } catch (IOException | UncheckedServletException e) { - log.error(e.getMessage(), e); - } - } + private static final Logger log = LoggerFactory.getLogger(Http11Processor.class); + + private final Socket connection; + private final HandlerMapping handlerMapping; + + public Http11Processor(final Socket connection, final HandlerMapping handlerMapping) { + this.connection = connection; + this.handlerMapping = handlerMapping; + } + + @Override + public void run() { + log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort()); + process(connection); + } + + @Override + public void process(final Socket connection) { + try (final var inputBuffer = new BufferedReader(new InputStreamReader(connection.getInputStream())); + final var outputBuffer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) { + final var requestReader = new Http11RequestReader(inputBuffer); + final var requestWriter = new Http11ResponseWriter(outputBuffer); + + final var request = requestReader.read(); + requestWriter.write(handlerMapping.handle(request)); + } catch (IOException | UncheckedServletException e) { + log.error(e.getMessage(), e); + } + } } diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11RequestReader.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11RequestReader.java new file mode 100644 index 0000000000..3ca300cde9 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11RequestReader.java @@ -0,0 +1,85 @@ +package org.apache.coyote.http11; + +import static java.util.stream.Collectors.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.coyote.Cookie; +import org.apache.coyote.HttpMethod; +import org.apache.coyote.HttpProtocolVersion; +import org.apache.coyote.request.Request; +import org.apache.coyote.request.RequestBody; +import org.apache.coyote.request.RequestHeader; +import org.apache.coyote.request.RequestLine; +import org.apache.coyote.request.RequestUri; +import org.apache.coyote.response.HeaderType; + +public class Http11RequestReader { + + private static final String REQUEST_LINE_DELIMITER = " "; + private static final String END_OF_LINE = ""; + private static final String HEADER_SEPARATOR = ": "; + private static final String COOKIE_DELIMITER = "; "; + private static final String COOKIE_KEY_VALUE_SEPARATOR = "="; + + private final BufferedReader reader; + + public Http11RequestReader(final BufferedReader reader) { + this.reader = reader; + } + + public Request read() throws IOException { + final var requestLine = readRequestLine(); + final var requestHeader = readRequestHeader(); + final var requestBody = readRequestBody(requestHeader); + return new Request(requestLine, requestHeader, requestBody); + } + + private RequestLine readRequestLine() throws IOException { + final var requestLine = reader.readLine(); + final var parts = requestLine.split(REQUEST_LINE_DELIMITER); + final var method = HttpMethod.from(parts[0]); + final var uri = RequestUri.from(parts[1]); + final var version = HttpProtocolVersion.version(parts[2]); + return new RequestLine(method, uri, version); + } + + private RequestHeader readRequestHeader() throws IOException { + final var headerString = new ArrayList(); + String line; + while ((!END_OF_LINE.equals(line = reader.readLine()))) { + headerString.add(line); + } + final var headers = headerString.stream() + .map(header -> header.split(HEADER_SEPARATOR, 2)) + .collect(toMap(parts -> parts[0], parts -> parts[1])); + final var cookie = headers.remove(HeaderType.COOKIE.getName()); + return new RequestHeader(headers, readCookies(cookie)); + } + + private List readCookies(final String cookieString) { + if (cookieString == null) { + return new ArrayList<>(); + } + return Arrays.stream(cookieString.split(COOKIE_DELIMITER)) + .map(field -> field.split(COOKIE_KEY_VALUE_SEPARATOR)) + .filter(parts -> parts.length == 2) + .map(parts -> new Cookie(parts[0], parts[1])) + .collect(toList()); + } + + private RequestBody readRequestBody(final RequestHeader header) throws IOException { + final var contentLength = header.find(HeaderType.CONTENT_LENGTH.getName()); + if (contentLength == null) { + return RequestBody.empty(); + } + final var bodySize = Integer.parseInt(contentLength); + final var buffer = new char[bodySize]; + reader.read(buffer, 0, bodySize); + return RequestBody.from(new String(buffer)); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseWriter.java b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseWriter.java new file mode 100644 index 0000000000..b621af80f2 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/http11/Http11ResponseWriter.java @@ -0,0 +1,70 @@ +package org.apache.coyote.http11; + +import static java.util.Comparator.*; +import static java.util.stream.Collectors.*; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.List; + +import org.apache.coyote.Cookie; +import org.apache.coyote.response.HeaderType; +import org.apache.coyote.response.Response; +import org.apache.coyote.response.ResponseHeader; +import org.apache.coyote.response.StatusLine; + +public class Http11ResponseWriter { + + private static final String CRLF = "\r\n"; + private static final String STATUS_LINE_DELIMITER = " "; + private static final String LINE_END = " "; + private static final String HEADER_SEPARATOR = ": "; + private static final String HEADER_BODY_SEPARATOR = ""; + private static final String COOKIE_DELIMITER = "; "; + private static final String COOKIE_KEY_VALUE_SEPARATOR = "="; + + private final BufferedWriter writer; + + public Http11ResponseWriter(final BufferedWriter writer) { + this.writer = writer; + } + + public void write(final Response response) throws IOException { + final var statusLine = response.getStatusLine(); + final var header = response.getHeader(); + final var responseBody = response.getResponseBody(); + + writer.write(String.join(CRLF, + format(statusLine), + format(header), + HEADER_BODY_SEPARATOR, + responseBody)); + + writer.flush(); + } + + private String format(final StatusLine statusLine) { + final var version = statusLine.getVersion().getVersion(); + final var code = statusLine.getCode(); + return String.join(STATUS_LINE_DELIMITER, version, Integer.toString(code.getCode()), code.getMessage()) + + LINE_END; + } + + private String format(final ResponseHeader responseHeader) { + final var headers = responseHeader.getHeaders(); + final var cookies = responseHeader.getCookies(); + if (!cookies.isEmpty()) { + headers.put(HeaderType.SET_COOKIE, format(cookies)); + } + return headers.entrySet().stream() + .sorted(comparingInt(e -> e.getKey().ordinal())) + .map(e -> String.join(HEADER_SEPARATOR, e.getKey().getName(), e.getValue()) + LINE_END) + .collect(joining(CRLF)); + } + + public String format(final List cookies) { + return cookies.stream() + .map(cookie -> cookie.getKey() + COOKIE_KEY_VALUE_SEPARATOR + cookie.getValue() + COOKIE_DELIMITER) + .collect(joining()); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/Request.java b/tomcat/src/main/java/org/apache/coyote/request/Request.java new file mode 100644 index 0000000000..27435d7eff --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/Request.java @@ -0,0 +1,45 @@ +package org.apache.coyote.request; + +import org.apache.coyote.HttpMethod; + +public class Request { + + private final RequestLine requestLine; + private final RequestHeader header; + private final RequestBody body; + + public Request(final RequestLine requestLine, final RequestHeader header, + final RequestBody body) { + this.requestLine = requestLine; + this.header = header; + this.body = body; + } + + public boolean hasPath(final String path) { + return requestLine.hasPath(path); + } + + public boolean hasMethod(final HttpMethod method) { + return requestLine.hasMethod(method); + } + + public String findBodyField(final String key) { + return body.findField(key); + } + + public String findQueryParam(final String key) { + return requestLine.findQueryParam(key); + } + + public String findCookie(final String key) { + return header.findCookie(key); + } + + public String findSession() { + return header.findSession(); + } + + public String getPath() { + return requestLine.getPath(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java new file mode 100644 index 0000000000..9de57b209a --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestBody.java @@ -0,0 +1,40 @@ +package org.apache.coyote.request; + +import static java.util.stream.Collectors.*; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class RequestBody { + + private static final String DELIMITER = "&"; + private static final String KEY_VALUE_SEPARATOR = "="; + + private final Map fields = new HashMap<>(); + + private RequestBody() { + } + + private RequestBody(final Map fields) { + this.fields.putAll(fields); + } + + public static RequestBody empty() { + return new RequestBody(); + } + + public static RequestBody from(String body) { + return Arrays.stream(body.split(DELIMITER)) + .map(field -> field.split(KEY_VALUE_SEPARATOR, 2)) + .filter(field -> field.length == 2) + .collect(collectingAndThen( + toMap(parts -> parts[0], field -> field[1]), + RequestBody::new + )); + } + + public String findField(final String key) { + return fields.get(key); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestHeader.java b/tomcat/src/main/java/org/apache/coyote/request/RequestHeader.java new file mode 100644 index 0000000000..8d0a10cacc --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestHeader.java @@ -0,0 +1,43 @@ +package org.apache.coyote.request; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.apache.coyote.Cookie; + +public class RequestHeader { + + private final Map headers = new HashMap<>(); + private final List cookies; + + public RequestHeader(Map headers, final List cookies) { + this.headers.putAll(headers); + this.cookies = cookies; + } + + public String find(final String key) { + return headers.get(key); + } + + public String findCookie(final String key) { + final var optionalCookie = cookies.stream() + .filter(cookie -> Objects.equals(key, cookie.getKey())) + .findAny(); + if (optionalCookie.isEmpty()) { + return null; + } + return optionalCookie.get().getValue(); + } + + public String findSession() { + final var optionalCookie = cookies.stream() + .filter(Cookie::isSession) + .findAny(); + if (optionalCookie.isEmpty()) { + return null; + } + return optionalCookie.get().getValue(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java new file mode 100644 index 0000000000..effcf02995 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestLine.java @@ -0,0 +1,33 @@ +package org.apache.coyote.request; + +import org.apache.coyote.HttpMethod; +import org.apache.coyote.HttpProtocolVersion; + +public class RequestLine { + + private final HttpMethod method; + private final RequestUri uri; + private final HttpProtocolVersion version; + + public RequestLine(final HttpMethod method, final RequestUri uri, final HttpProtocolVersion version) { + this.method = method; + this.uri = uri; + this.version = version; + } + + public boolean hasPath(final String path) { + return uri.hasPath(path); + } + + public boolean hasMethod(final HttpMethod method) { + return this.method.equals(method); + } + + public String findQueryParam(final String key) { + return uri.findQueryParam(key); + } + + public String getPath() { + return uri.getPath(); + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/request/RequestUri.java b/tomcat/src/main/java/org/apache/coyote/request/RequestUri.java new file mode 100644 index 0000000000..7fd102a29f --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/request/RequestUri.java @@ -0,0 +1,64 @@ +package org.apache.coyote.request; + +import static java.util.stream.Collectors.*; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class RequestUri { + + private static final String QUERY_STRING_SEPARATOR = "?"; + private static final String QUERY_PARAM_DELIMITER = "&"; + private static final String KEY_VALUE_SEPARATOR = "="; + + private final String path; + private final Map queryParams = new HashMap<>(); + + private RequestUri(final String path) { + this(path, new HashMap<>()); + } + + private RequestUri(final String path, final Map queryParams) { + this.path = path; + this.queryParams.putAll(queryParams); + } + + public static RequestUri from(String uri) { + final var queryStringIndex = uri.indexOf(QUERY_STRING_SEPARATOR); + + if (hasNoQuery(queryStringIndex)) { + return new RequestUri(uri); + } + + final var path = uri.substring(0, queryStringIndex); + final var queryString = uri.substring(queryStringIndex + 1); + final var queryParams = parseQueryString(queryString); + + return new RequestUri(path, queryParams); + } + + private static Map parseQueryString(final String queryString) { + return Arrays.stream(queryString.split(QUERY_PARAM_DELIMITER)) + .map(queryParam -> queryParam.split(KEY_VALUE_SEPARATOR, 2)) + .filter(parts -> parts.length == 2) + .collect(toMap(parts -> parts[0], parts -> parts[1])); + } + + private static boolean hasNoQuery(final int index) { + return index == -1; + } + + public boolean hasPath(final String path) { + return Objects.equals(this.path, path); + } + + public String findQueryParam(final String key) { + return queryParams.get(key); + } + + public String getPath() { + return path; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/HeaderType.java b/tomcat/src/main/java/org/apache/coyote/response/HeaderType.java new file mode 100644 index 0000000000..4937ffe708 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/HeaderType.java @@ -0,0 +1,19 @@ +package org.apache.coyote.response; + +public enum HeaderType { + CONTENT_TYPE("Content-Type"), + CONTENT_LENGTH("Content-Length"), + LOCATION("Location"), + SET_COOKIE("Set-Cookie"), + COOKIE("Cookie"); + + private final String name; + + HeaderType(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/Response.java b/tomcat/src/main/java/org/apache/coyote/response/Response.java new file mode 100644 index 0000000000..d8cf17e55e --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/Response.java @@ -0,0 +1,87 @@ +package org.apache.coyote.response; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.coyote.Cookie; +import org.apache.coyote.MimeType; + +import nextstep.jwp.handler.ResourceProvider; + +public class Response { + + private final StatusLine statusLine; + private final ResponseHeader header; + private final String responseBody; + + public Response(final StatusLine statusLine, final ResponseHeader header, final String responseBody) { + this.statusLine = statusLine; + this.header = header; + this.responseBody = responseBody; + } + + public static Response ok(final String responseBody, final MimeType mimeType) { + return of(StatusCode.OK, responseBody, mimeType); + } + + public static Response badRequest() { + return code(StatusCode.BAD_REQUEST); + } + + public static Response unauthorized() { + return code(StatusCode.UNAUTHORIZED); + } + + public static Response notFound() { + return code(StatusCode.NOT_FOUND); + } + + public static Response redirect(final String location) { + final var statusLine = new StatusLine(StatusCode.FOUND); + final var header = makeHeader(); + header.add(HeaderType.LOCATION, location); + return new Response(statusLine, header, null); + } + + private static ResponseHeader makeHeader() { + return makeHeader("", MimeType.HTML); + } + + private static ResponseHeader makeHeader(final String responseBody, final MimeType mimeType) { + final Map headers = new HashMap<>(); + headers.put(HeaderType.CONTENT_TYPE, mimeType.getValue()); + headers.put(HeaderType.CONTENT_LENGTH, Integer.toString(responseBody.getBytes().length)); + return new ResponseHeader(headers); + } + + private static Response code(final StatusCode code) { + final var resourcePath = code.getResourcePath(); + if (resourcePath.isEmpty()) { + return of(code, resourcePath, MimeType.HTML); + } + return of(code, ResourceProvider.provide(resourcePath), MimeType.HTML); + } + + private static Response of(final StatusCode code, final String responseBody, final MimeType mimeType) { + final var statusLine = new StatusLine(code); + final var header = makeHeader(responseBody, mimeType); + return new Response(statusLine, header, responseBody); + } + + public Response addCookie(final Cookie cookie) { + header.addCookie(cookie); + return this; + } + + public StatusLine getStatusLine() { + return statusLine; + } + + public ResponseHeader getHeader() { + return header; + } + + public String getResponseBody() { + return responseBody; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/ResponseHeader.java b/tomcat/src/main/java/org/apache/coyote/response/ResponseHeader.java new file mode 100644 index 0000000000..393f87ddc5 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/ResponseHeader.java @@ -0,0 +1,53 @@ +package org.apache.coyote.response; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.coyote.Cookie; +import org.apache.coyote.MimeType; + +public class ResponseHeader { + + private static final Set COMMON_HEADERS = Set.of( + HeaderType.CONTENT_TYPE, + HeaderType.CONTENT_LENGTH, + HeaderType.SET_COOKIE + ); + + private final Map headers = new HashMap<>(); + private final List cookies = new ArrayList<>(); + + public ResponseHeader(final Map headers) { + this.headers.putAll(headers); + } + + public void addContentLength(final MimeType mimeType) { + headers.put(HeaderType.CONTENT_LENGTH, mimeType.getValue()); + } + + public void addContentLength(final String responseBody) { + headers.put(HeaderType.CONTENT_LENGTH, Integer.toString(responseBody.getBytes().length)); + } + + public void addCookie(final Cookie cookie) { + cookies.add(cookie); + } + + public void add(HeaderType key, String value) { + if (COMMON_HEADERS.contains(key)) { + return; + } + headers.put(key, value); + } + + public Map getHeaders() { + return headers; + } + + public List getCookies() { + return cookies; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/StatusCode.java b/tomcat/src/main/java/org/apache/coyote/response/StatusCode.java new file mode 100644 index 0000000000..5cf1477616 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/StatusCode.java @@ -0,0 +1,36 @@ +package org.apache.coyote.response; + +public enum StatusCode { + OK(200, "OK"), + FOUND(302, "Found"), + BAD_REQUEST(400, "Bad Request", "/400.html"), + UNAUTHORIZED(401, "Unauthorized", "/401.html"), + NOT_FOUND(404, "Not Found", "/404.html"), + ; + + private final int code; + private final String message; + private final String resourcePath; + + StatusCode(final int code, final String message) { + this(code, message, ""); + } + + StatusCode(final int code, final String message, final String resourcePath) { + this.code = code; + this.message = message; + this.resourcePath = resourcePath; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public String getResourcePath() { + return resourcePath; + } +} diff --git a/tomcat/src/main/java/org/apache/coyote/response/StatusLine.java b/tomcat/src/main/java/org/apache/coyote/response/StatusLine.java new file mode 100644 index 0000000000..32d4c2e801 --- /dev/null +++ b/tomcat/src/main/java/org/apache/coyote/response/StatusLine.java @@ -0,0 +1,22 @@ +package org.apache.coyote.response; + +import org.apache.coyote.HttpProtocolVersion; + +public class StatusLine { + + private final HttpProtocolVersion version; + private final StatusCode code; + + public StatusLine(final StatusCode code) { + this.version = HttpProtocolVersion.HTTP11; + this.code = code; + } + + public HttpProtocolVersion getVersion() { + return version; + } + + public StatusCode getCode() { + return code; + } +} diff --git a/tomcat/src/main/resources/static/400.html b/tomcat/src/main/resources/static/400.html new file mode 100644 index 0000000000..0160ebbf9c --- /dev/null +++ b/tomcat/src/main/resources/static/400.html @@ -0,0 +1,54 @@ + + + + + + + + + 404 Error - SB Admin + + + + +
+
+
+
+
+
+
+

400

+

Bad Request

+

Access to this resource is denied.

+ + + Return to Dashboard + +
+
+
+
+
+
+ +
+ + + + diff --git a/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg b/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg index f0d345f991..3807b05966 100644 --- a/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg +++ b/tomcat/src/main/resources/static/assets/img/error-404-monochrome.svg @@ -1 +1,67 @@ -error-404-monochrome \ No newline at end of file + + + + + + + + + + + + + + + + error-404-monochrome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tomcat/src/main/resources/static/login.html b/tomcat/src/main/resources/static/login.html index f4ed9de875..ff5bdacd5d 100644 --- a/tomcat/src/main/resources/static/login.html +++ b/tomcat/src/main/resources/static/login.html @@ -1,66 +1,70 @@ - - - - - - - 로그인 - - - - -
-
-
-
-
-
-
-

로그인

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

로그인

+
+
+
+ +
- +
+ +
+
+
+
-
+
-
+
+
- - - - + + + + + + diff --git a/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java b/tomcat/src/test/java/nextstep/org/apache/coyote/http11/Http11ProcessorTest.java index 512b919f09..21cfb7bdf7 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,62 +1,83 @@ package nextstep.org.apache.coyote.http11; -import support.StubSocket; -import org.apache.coyote.http11.Http11Processor; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; +import org.apache.catalina.HandlerMapping; +import org.apache.coyote.http11.Http11Processor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import nextstep.jwp.handler.BaseRequestHandler; +import nextstep.jwp.handler.LoginRequestHandler; +import nextstep.jwp.handler.RegisterRequestHandler; +import nextstep.jwp.handler.StaticContentRequestHandler; +import support.StubSocket; class Http11ProcessorTest { - @Test - void process() { - // given - final var socket = new StubSocket(); - final var processor = new Http11Processor(socket); - - // when - 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); - } - - @Test - void index() throws IOException { - // given - final String httpRequest= String.join("\r\n", - "GET /index.html HTTP/1.1 ", - "Host: localhost:8080 ", - "Connection: keep-alive ", - "", - ""); - - final var socket = new StubSocket(httpRequest); - final Http11Processor processor = new Http11Processor(socket); - - // when - processor.process(socket); - - // then - final URL resource = getClass().getClassLoader().getResource("static/index.html"); - var expected = "HTTP/1.1 200 OK \r\n" + - "Content-Type: text/html;charset=utf-8 \r\n" + - "Content-Length: 5564 \r\n" + - "\r\n"+ - new String(Files.readAllBytes(new File(resource.getFile()).toPath())); - - assertThat(socket.output()).isEqualTo(expected); - } + private HandlerMapping handlerMapping; + + @BeforeEach + void setUp() { + final var handlers = Set.of( + new BaseRequestHandler(), + new LoginRequestHandler(), + new RegisterRequestHandler() + ); + final var defaultHandler = new StaticContentRequestHandler(); + handlerMapping = new HandlerMapping(handlers, defaultHandler); + } + + @Test + void process() { + // given + final var socket = new StubSocket(); + final var processor = new Http11Processor(socket, handlerMapping); + + // when + 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); + } + + @Test + void index() throws IOException { + // given + final String httpRequest = String.join("\r\n", + "GET /index.html HTTP/1.1 ", + "Host: localhost:8080 ", + "Connection: keep-alive ", + "", + ""); + + final var socket = new StubSocket(httpRequest); + final Http11Processor processor = new Http11Processor(socket, handlerMapping); + + // when + processor.process(socket); + + // then + final URL resource = getClass().getClassLoader().getResource("static/index.html"); + var expected = "HTTP/1.1 200 OK \r\n" + + "Content-Type: text/html;charset=utf-8 \r\n" + + "Content-Length: 5564 \r\n" + + "\r\n" + + new String(Files.readAllBytes(new File(resource.getFile()).toPath())); + + assertThat(socket.output()).isEqualTo(expected); + } }